From b3a2c4df8456160035d4048e4bd0bdcdba1f25f2 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 11:39:26 +0200 Subject: [PATCH 01/47] feat(core): controlled stepper --- .../components/SimpleStepper/SimpleStepper.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 => { From 2fb51621c65bc25ba3ef46475699bc206bea671e Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 11:42:17 +0200 Subject: [PATCH 02/47] feat(scaffolder): mocked fe flow --- package.json | 2 +- packages/app/src/apis.ts | 16 +- .../react-ssr-template/template.yaml | 5 + plugins/scaffolder/package.json | 4 +- plugins/scaffolder/src/api.ts | 52 ++++++ .../src/components/CreatePage/CreatePage.tsx | 111 ++++++++++++ .../src/components/CreatePage/index.ts | 1 + .../components/JobStatusModal/JobStage.tsx | 149 ++++++++++++++++ .../JobStatusModal/JobStatusModal.tsx | 41 +++++ .../src/components/JobStatusModal/index.ts | 1 + .../src/components/JobStatusModal/jobMocks.ts | 78 +++++++++ .../src/components/JobStatusModal/types.ts | 23 +++ .../{ => ScaffolderPage}/TemplateCard.tsx | 18 +- .../src/components/ScaffolderPage/index.tsx | 7 +- plugins/scaffolder/src/index.ts | 3 +- plugins/scaffolder/src/plugin.ts | 13 +- plugins/scaffolder/src/routes.ts | 12 ++ yarn.lock | 162 +++++++++++++++++- 18 files changed, 669 insertions(+), 29 deletions(-) create mode 100644 plugins/scaffolder/src/api.ts create mode 100644 plugins/scaffolder/src/components/CreatePage/CreatePage.tsx create mode 100644 plugins/scaffolder/src/components/CreatePage/index.ts create mode 100644 plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx create mode 100644 plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx create mode 100644 plugins/scaffolder/src/components/JobStatusModal/index.ts create mode 100644 plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts create mode 100644 plugins/scaffolder/src/components/JobStatusModal/types.ts rename plugins/scaffolder/src/components/{ => ScaffolderPage}/TemplateCard.tsx (78%) create mode 100644 plugins/scaffolder/src/routes.ts 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 d2b2706464..1565dc294e 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -51,6 +51,10 @@ import { graphQlBrowseApiRef, GraphQLEndpoints, } from '@backstage/plugin-graphiql'; +import { + scaffolderApiRef, + ScaffolderApi, +} from '@backstage/plugin-scaffolder/src/api'; export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console @@ -113,8 +117,16 @@ export const apis = (config: ConfigApi) => { builder.add( catalogApiRef, new CatalogClient({ - apiOrigin: 'http://localhost:3000', - basePath: '/catalog/api', + apiOrigin: 'http://localhost:7000', + basePath: '/catalog', + }), + ); + + builder.add( + scaffolderApiRef, + new ScaffolderApi({ + apiOrigin: 'http://localhost:7000', + basePath: '/scaffolder/v1', }), ); 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..e01eb4cb45 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,8 @@ spec: processor: cookiecutter type: website path: '.' + parameters: + component_name: + title: Component name + type: string + description: Name of the component diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 1bccd6a738..28ed3dcc94 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -22,12 +22,14 @@ }, "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", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-alpha.5", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts new file mode 100644 index 0000000000..f87285a80c --- /dev/null +++ b/plugins/scaffolder/src/api.ts @@ -0,0 +1,52 @@ +/* + * 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; + } + + async scaffold( + template: TemplateEntityV1alpha1, + values: Record, + ) { + const url = `${this.apiOrigin}${this.basePath}/jobs`; + const jobId = await fetch(url, { + method: 'POST', + body: JSON.stringify({ template, values }), + }).then(x => x.json()); + + return jobId; + } +} diff --git a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx new file mode 100644 index 0000000000..f67a6dbd8e --- /dev/null +++ b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import useStaleWhileRevalidate from 'swr'; +import { useParams } from 'react-router-dom'; +import { LinearProgress, Button } from '@material-ui/core'; +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { + useApi, + SimpleStepper, + SimpleStepperStep, + Page, + Content, + ContentHeader, + Header, + Lifecycle, +} from '@backstage/core'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { withTheme, IChangeEvent } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; +import { JobStatusModal } from '../JobStatusModal'; +import { scaffolderApiRef } from '../../api'; + +const Form = withTheme(MuiTheme); + +export const CreatePage = () => { + const catalogApi = useApi(catalogApiRef); + const scaffolderApi = useApi(scaffolderApiRef); + const { templateName } = useParams(); + const { + data: [template] = [] as TemplateEntityV1alpha1[], + isValidating, + } = useStaleWhileRevalidate( + `templates/${templateName}`, + async () => + (catalogApi.getEntities({ + kind: 'Template', + 'metadata.name': templateName, + }) as any) as Promise, + ); + const [formState, setFormState] = useState({}); + + const handleChange = (e: IChangeEvent) => + setFormState({ ...formState, ...e.formData }); + + const [jobId, setJobId] = useState(null); + const handleClose = () => setJobId(null); + if (!template && isValidating) return ; + if (!template || !template?.spec?.parameters) return null; + + const handleCreate = async () => { + const job = await scaffolderApi.scaffold(template, formState); + setJobId(job); + }; + + return ( + +
+ Create a new component + + } + subtitle="Create new software components using standard templates" + /> + + + {jobId && } + { + if (nextStep === 2) { + handleCreate(); + } + }} + > + +
+
+ diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index 2f9774757b..1570006b44 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -35,11 +35,11 @@ import { LinearProgress, } from '@material-ui/core'; import { Link as RouterLink } from 'react-router-dom'; -import TemplateCard from '../TemplateCard'; +import TemplateCard from './TemplateCard'; import useStaleWhileRevalidate from 'swr'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -const ScaffolderPage: React.FC<{}> = () => { +export const ScaffolderPage: React.FC<{}> = () => { const catalogApi = useApi(catalogApiRef); const errorApi = useApi(errorApiRef); @@ -98,6 +98,7 @@ const ScaffolderPage: React.FC<{}> = () => { = () => { ); }; - -export default ScaffolderPage; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 5b20cb0158..3fd7d38c57 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { plugin, rootRoute } from './plugin'; +export { plugin } from './plugin'; +export { rootRoute, createTemplateRoute } from './routes'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index b2cb305e57..d0d38d5812 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 { CreatePage } from './components/CreatePage'; +import { rootRoute, createTemplateRoute } from './routes'; export const plugin = createPlugin({ id: 'scaffolder', register({ router }) { router.addRoute(rootRoute, ScaffolderPage); + router.addRoute(createTemplateRoute, CreatePage); }, }); diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts new file mode 100644 index 0000000000..f42f9540f9 --- /dev/null +++ b/plugins/scaffolder/src/routes.ts @@ -0,0 +1,12 @@ +import { createRouteRef } from '@backstage/core'; + +export const rootRoute = createRouteRef({ + icon: () => null, + path: '/create', + title: 'Create new entity', +}); +export const createTemplateRoute = createRouteRef({ + icon: () => null, + path: '/create/:templateName', + title: 'Entity creation', +}); diff --git a/yarn.lock b/yarn.lock index f4fa34b993..08660c27b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -876,6 +876,14 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" +"@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" @@ -2369,6 +2377,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" @@ -2476,10 +2506,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" @@ -3568,6 +3598,11 @@ 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": + 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== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -4434,7 +4469,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.0, ajv@^6.5.5: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, 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== @@ -4725,7 +4760,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= @@ -6295,6 +6330,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" @@ -6544,7 +6598,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.6.5: +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== @@ -6554,6 +6608,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" @@ -11559,6 +11618,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" @@ -11633,6 +11708,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" @@ -13002,6 +13082,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" @@ -14923,6 +15008,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" @@ -15133,6 +15225,13 @@ raf-schd@^4.0.0: 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" @@ -15211,6 +15310,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@11.0.3: version "11.0.3" resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#5678bb3e725d8b56cb7cf57f56e952105fc4f2af" @@ -16714,6 +16825,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" @@ -18710,6 +18828,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= + value-equal@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" From 01b3e44429d9aab6125b2b93eaafa4340e2cfd88 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 14:02:51 +0200 Subject: [PATCH 03/47] feat(scaffolder): do real fetch --- plugins/scaffolder/src/api.ts | 9 ++- .../components/JobStatusModal/JobStage.tsx | 27 ++++--- .../JobStatusModal/JobStatusModal.tsx | 7 +- .../src/components/JobStatusModal/jobMocks.ts | 78 ------------------- .../JobStatusModal/useJobPolling.ts | 41 ++++++++++ 5 files changed, 66 insertions(+), 96 deletions(-) delete mode 100644 plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts create mode 100644 plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index f87285a80c..04588d7177 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -42,11 +42,18 @@ export class ScaffolderApi { values: Record, ) { const url = `${this.apiOrigin}${this.basePath}/jobs`; - const jobId = await fetch(url, { + const { id: jobId } = await fetch(url, { method: 'POST', body: JSON.stringify({ template, values }), }).then(x => x.json()); return jobId; } + + 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/JobStatusModal/JobStage.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx index f1dec09f88..ef799d13b4 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx @@ -87,16 +87,10 @@ type Props = { className?: string; log: string[]; startedAt: string; - finishedAt?: string; + endedAt?: string; status?: Job['status']; }; -export const JobStage = ({ - finishedAt, - startedAt, - name, - log, - status, -}: Props) => { +export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { const classes = useStyles(); const [expanded, setExpanded] = useState(false); @@ -104,18 +98,23 @@ export const JobStage = ({ if (status === 'FAILED') setExpanded(true); }, [status === 'FAILED', setExpanded]); - const timeElapsed = moment - .duration(moment(finishedAt ?? moment()).diff(moment(startedAt))) - .humanize(); + const timeElapsed = + status !== 'PENDING' + ? moment + .duration(moment(endedAt ?? moment()).diff(moment(startedAt))) + .humanize() + : null; return ( - {name} ({timeElapsed}) + {name} {timeElapsed && `(${timeElapsed})`} diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx index f946df1bf3..c589e01127 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -6,7 +6,7 @@ import { DialogContent, } from '@material-ui/core'; import { JobStage } from './JobStage'; -import { useJob } from './jobMocks'; +import { useJobPolling } from './useJobPolling'; type Props = { onClose: () => void; @@ -14,7 +14,8 @@ type Props = { }; export const JobStatusModal = ({ onClose, jobId }: Props) => { - const job = useJob(jobId); + console.log({ jobId }); + const job = useJobPolling(jobId); return ( @@ -30,7 +31,7 @@ export const JobStatusModal = ({ onClose, jobId }: Props) => { name={step.name} key={step.name} startedAt={step.startedAt} - finishedAt={step.finishedAt} + endedAt={step.endedAt} status={step.status} /> )) diff --git a/plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts b/plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts deleted file mode 100644 index 08a3d3b720..0000000000 --- a/plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { useMemo, useState, useEffect } from 'react'; -import { Job } from './types'; - -function* emulatePoll() { - const now = () => new Date().toString(); - const job: Job = { - id: '132536-42362-4253532', - metadata: { entity: {}, values: {} }, - status: 'STARTED', - stages: [ - { - name: 'created', - startedAt: now(), - log: [ - 'Job id #rw-tstywe-tdsy was successfully created and placed in the queue', - ], - status: 'STARTED', - }, - ], - }; - let newTime = now(); - job.stages[0].finishedAt = newTime; - job.stages.push({ - startedAt: newTime, - name: 'preparing', - log: ['preparing blahblah', 'some other stuuff'], - status: 'COMPLETE', - }); - yield job; - - newTime = now(); - job.stages[1].finishedAt = newTime; - job.stages.push({ - startedAt: newTime, - name: 'templating', - log: ['templating blahblah', 'some other stuuff'], - status: 'COMPLETE', - }); - yield job; - - newTime = now(); - job.stages[2].finishedAt = newTime; - job.stages.push({ - startedAt: newTime, - name: 'pushing', - log: ['pushing blahblah', 'some other stuuff'], - status: 'STARTED', - }); - yield job; - yield job; - job.stages[3].status = 'FAILED'; - job.stages[3].log.push('ERROR OCCURED'); - - while (true) yield job; -} - -export const useJob = (jobId: string | null) => { - const apiMock = useMemo(() => emulatePoll(), [jobId]); - const [job, setJob] = useState(undefined); - useEffect(() => { - if (!jobId) return; - const nextJobState = apiMock.next().value as Job; - setJob({ ...nextJobState }); - const intervalId = setInterval(() => { - const nextJobState = apiMock.next().value as Job; - - if (nextJobState?.status === 'FAILED') { - clearInterval(intervalId); - } - - setJob({ ...nextJobState }); - }, 3000); - return () => { - clearInterval(intervalId); - }; - }, [jobId, setJob]); - return job; -}; diff --git a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts new file mode 100644 index 0000000000..89b7228a9c --- /dev/null +++ b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'react'; +import { Job } from './types'; +import { useApi } from '@backstage/core'; +import { scaffolderApiRef } from '../../api'; + +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) => { + const scaffolderApi = useApi(scaffolderApiRef); + + const [job, setJob] = useState(undefined); + useEffect(() => { + if (!jobId) return; + const stopPolling = poll(async () => { + const nextJobState = await scaffolderApi.getJob(jobId); + if ( + nextJobState.status === 'FAILED' || + nextJobState.status === 'COMPLETED' + ) { + stopPolling(); + } + setJob(nextJobState); + }, 500); + return () => { + stopPolling(); + }; + }, [jobId, setJob]); + return job; +}; From 4aa3116476c80bcf8eadbdad6e39b8703c46a666 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 17 Jun 2020 21:21:32 +0200 Subject: [PATCH 04/47] feat(scaffolder): load data from catalog api --- plugins/scaffolder/package.json | 2 + .../src/components/ScaffolderPage/index.tsx | 79 +++++++++++-------- .../src/components/TemplateCard.tsx | 2 +- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 65322c31d6..3883dfb972 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -23,6 +23,8 @@ "dependencies": { "@backstage/core": "^0.1.1-alpha.12", "@backstage/theme": "^0.1.1-alpha.12", + "@backstage/catalog-model": "^0.1.1-alpha.12", + "@backstage/plugin-catalog": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index d675b59f52..1dbd993d93 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -23,33 +23,38 @@ import { SupportButton, Page, pageTheme, + useApi, } from '@backstage/core'; -import { Button, Grid, Link, Typography } from '@material-ui/core'; +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { + Typography, + Link, + Button, + Grid, + LinearProgress, +} from '@material-ui/core'; import { Link as RouterLink } from 'react-router-dom'; import TemplateCard from '../TemplateCard'; +import { useAsync } from 'react-use'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -// TODO(blam): Connect to backend -const STATIC_DATA = [ - { - id: 'springboot-template', - type: 'service', - name: 'Spring Boot Service', - tags: ['Recommended', 'Java'], - description: - 'Standard Spring Boot (Java) microservice with recommended configuration.', - ownerId: 'spotify', - }, - { - id: 'react-ssr-template', - type: 'website', - name: 'SSR React Website', - tags: ['Recommended', 'React'], - description: - 'Next.js application skeleton for creating isomorphic web applications.', - ownerId: 'spotify', - }, -]; const ScaffolderPage: React.FC<{}> = () => { + const catalogApi = useApi(catalogApiRef); + + const { value, loading } = useAsync(async () => { + const entities = await catalogApi.getEntities({ kind: 'Template' }); + return (entities as TemplateEntityV1alpha1[]).map(template => ({ + id: template.metadata.uid, + type: template.spec.type, + name: template.metadata.name, + // TODO(shmidt-i): decide on tags + tags: ['not-implemented'], + description: template.metadata.description ?? '-', + // TODO(shmidt-i): decide on owner + ownerId: '-', + })); + }); + return (
= () => { . - - {STATIC_DATA.map(item => { - return ( - - ); - })} - + {loading ? ( + + ) : ( + + {value!.map(item => { + return ( + + ); + })} + + )} ); diff --git a/plugins/scaffolder/src/components/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard.tsx index 56d8907f7b..618eda64f1 100644 --- a/plugins/scaffolder/src/components/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard.tsx @@ -67,7 +67,7 @@ const TemplateCard: FC = ({
{tags?.map(tag => ( - + ))} {description} From 74b237d495791d5b25168c576528547152ea0a50 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Sat, 20 Jun 2020 13:55:22 +0200 Subject: [PATCH 05/47] feat(scaffolder): use swr, pretty template mocks, move out grid item --- .../react-ssr-template/template.yaml | 5 +- .../springboot-template/template.yaml | 12 ++++ plugins/scaffolder-backend/scripts/mock-data | 16 +++-- plugins/scaffolder/package.json | 3 +- .../src/components/ScaffolderPage/index.tsx | 62 ++++++++++--------- .../src/components/TemplateCard.tsx | 43 +++++-------- 6 files changed, 78 insertions(+), 63 deletions(-) create mode 100644 plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml 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 6834f0d1b6..058e2a715f 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml @@ -4,6 +4,9 @@ metadata: name: react-ssr-template title: React SSR Template description: Next.js application skeleton for creating isomorphic web applications. + annotations: + tags: Recommended,React spec: - type: cookiecutter + processor: cookiecutter + type: website path: '.' diff --git a/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml b/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml new file mode 100644 index 0000000000..d44d926ef4 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Template +metadata: + name: springboot-template + title: Spring Boot Service + description: Standard Spring Boot (Java) microservice with recommended configuration. + annotations: + tags: Recommended,Java +spec: + processor: cookiecutter + type: service + path: '.' diff --git a/plugins/scaffolder-backend/scripts/mock-data b/plugins/scaffolder-backend/scripts/mock-data index be3fa2b6cf..594bd76607 100755 --- a/plugins/scaffolder-backend/scripts/mock-data +++ b/plugins/scaffolder-backend/scripts/mock-data @@ -1,8 +1,12 @@ #!/usr/bin/env bash -curl \ ---location \ ---request POST 'localhost:7000/catalog/locations' \ ---header 'Content-Type: application/json' \ ---data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/sample-templates/react-ssr-template/template.yaml\"}" - +for URL in \ + 'react-ssr-template' \ + 'springboot-template' \ +; do \ + curl \ + --location \ + --request POST 'localhost:7000/catalog/locations' \ + --header 'Content-Type: application/json' \ + --data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/sample-templates/${URL}/template.yaml\"}" +done diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 3883dfb972..6bd312921e 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -31,7 +31,8 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-alpha.5", - "react-use": "^14.2.0" + "react-use": "^14.2.0", + "swr": "^0.2.2" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index 1dbd993d93..a6dbac8f83 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { Lifecycle, Content, @@ -24,6 +24,7 @@ import { Page, pageTheme, useApi, + errorApiRef, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; import { @@ -35,25 +36,25 @@ import { } from '@material-ui/core'; import { Link as RouterLink } from 'react-router-dom'; import TemplateCard from '../TemplateCard'; -import { useAsync } from 'react-use'; +import useStaleWhileRevalidate from 'swr'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; const ScaffolderPage: React.FC<{}> = () => { const catalogApi = useApi(catalogApiRef); + const errorApi = useApi(errorApiRef); - const { value, loading } = useAsync(async () => { - const entities = await catalogApi.getEntities({ kind: 'Template' }); - return (entities as TemplateEntityV1alpha1[]).map(template => ({ - id: template.metadata.uid, - type: template.spec.type, - name: template.metadata.name, - // TODO(shmidt-i): decide on tags - tags: ['not-implemented'], - description: template.metadata.description ?? '-', - // TODO(shmidt-i): decide on owner - ownerId: '-', - })); - }); + const { data: templates, isValidating, error } = useStaleWhileRevalidate( + 'templates/all', + async () => + catalogApi.getEntities({ kind: 'Template' }) as Promise< + TemplateEntityV1alpha1[] + >, + ); + + useEffect(() => { + if (!error) return; + errorApi.post(error); + }, [error, errorApi]); return ( @@ -89,23 +90,26 @@ const ScaffolderPage: React.FC<{}> = () => { . - {loading ? ( - - ) : ( - - {value!.map(item => { + {isValidating && } + + {templates && + templates.map(template => { return ( - + + + ); })} - - )} + ); diff --git a/plugins/scaffolder/src/components/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard.tsx index 618eda64f1..6d47f05693 100644 --- a/plugins/scaffolder/src/components/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard.tsx @@ -14,14 +14,7 @@ * limitations under the License. */ import React, { FC } from 'react'; -import { - Button, - Card, - Chip, - Grid, - Typography, - makeStyles, -} from '@material-ui/core'; +import { Button, Card, Chip, Typography, makeStyles } from '@material-ui/core'; const useStyles = makeStyles(theme => ({ header: { @@ -59,25 +52,23 @@ const TemplateCard: FC = ({ const classes = useStyles(); return ( - - -
- {type} - {title} + +
+ {type} + {title} +
+
+ {tags?.map(tag => ( + + ))} + + {description} + +
+
-
- {tags?.map(tag => ( - - ))} - - {description} - -
- -
-
- - +
+
); }; From 1b15895bc24d34087480ed21391986abb584d5fd Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 24 Jun 2020 23:36:09 +0200 Subject: [PATCH 06/47] feat(scaffolder): tags as list --- .../sample-templates/react-ssr-template/template.yaml | 5 +++-- .../sample-templates/springboot-template/template.yaml | 5 +++-- plugins/scaffolder/src/components/ScaffolderPage/index.tsx | 3 +-- 3 files changed, 7 insertions(+), 6 deletions(-) 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 058e2a715f..124d1575ce 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml @@ -4,8 +4,9 @@ metadata: name: react-ssr-template title: React SSR Template description: Next.js application skeleton for creating isomorphic web applications. - annotations: - tags: Recommended,React + tags: + - Recommended + - React spec: processor: cookiecutter type: website diff --git a/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml b/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml index d44d926ef4..681e8e7f51 100644 --- a/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml @@ -4,8 +4,9 @@ metadata: name: springboot-template title: Spring Boot Service description: Standard Spring Boot (Java) microservice with recommended configuration. - annotations: - tags: Recommended,Java + tags: + - Recommended + - Java spec: processor: cookiecutter type: service diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index a6dbac8f83..aa2a8b639a 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -103,8 +103,7 @@ const ScaffolderPage: React.FC<{}> = () => { }`} type={template.spec.type ?? ''} description={template.metadata.description ?? '-'} - // TODO(shmidt-i): how to store tags - tags={template.metadata.annotations?.tags.split(',') ?? []} + tags={template.metadata?.tags ?? []} /> ); From 6d482bd9470f34fa14446eaaec0075c2ece92df8 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Thu, 25 Jun 2020 17:18:46 +0200 Subject: [PATCH 07/47] feat(techdocs-reader): fetch and basic transform on html --- .../techdocs/src/reader/components/Reader.tsx | 832 ++---------------- 1 file changed, 49 insertions(+), 783 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index e306a72424..5517c0c715 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -16,796 +16,62 @@ import React from 'react'; import { useShadowDom } from '..'; - -const mockHtml: string = ` - - - - - - - - - - - - - - - - - - - - - - - - - - Download boilerplate - MkDocs Material Boilerplate - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
- -
- -
- - - - - - - - - - - - -
-
- - -
-
-
- -
-
-
- - -
-
-
- - -
-
-
- - -
-
- - - - - - - - - - -

Download boilerplate

-

Git clone

-
git clone https://github.com/peaceiris/mkdocs-material-boilerplate.git
-cd mkdocs-material-boilerplate
-
- - -

Download zip

-
wget 'https://github.com/peaceiris/mkdocs-material-boilerplate/archive/master.zip'
-unzip master.zip
-cd mkdocs-material-boilerplate-master
-
- - -

👉 Click me to download zip

- - - - - - - -
-
-
-
- - - - -
- - - - - - - - - - - -`; +import { useAsync } from 'react-use'; + +const useFetch = (url: string) => { + const state = useAsync(async () => { + const response = await fetch(url); + const raw = await response.text(); + return raw; + }, [url]); + + return state; +}; + +const addBaseUrl = (htmlString: string, baseUrl: string): string => { + const domParser = new DOMParser().parseFromString(htmlString, 'text/html'); + + const updateDom = ( + list: Array, + attributeName: string, + ): void => { + Array.from(list).forEach((elem: T) => { + const newUrl = new URL( + elem.getAttribute(attributeName)!, + baseUrl, + ).toString(); + elem.setAttribute(attributeName, newUrl); + }); + }; + + updateDom(Array.from(domParser.images), 'src'); + updateDom( + Array.from(domParser.links), + 'href', + ); + updateDom( + Array.from(domParser.querySelectorAll('link')), + 'href', + ); + + return domParser.body.parentElement?.outerHTML || htmlString; +}; export const Reader = () => { const shadowDomRef = useShadowDom(); + const state = useFetch( + 'https://techdocs-mock-sites.storage.googleapis.com/mkdocs/index.html', + ); React.useEffect(() => { const divElement = shadowDomRef.current; - if (!divElement?.shadowRoot) { - return; + if (divElement?.shadowRoot && state.value) { + divElement.shadowRoot.innerHTML = addBaseUrl( + state.value, + 'https://techdocs-mock-sites.storage.googleapis.com/mkdocs/', + ); } - divElement.shadowRoot.innerHTML = mockHtml; - }, [shadowDomRef]); + }, [shadowDomRef, state]); return ( <> From 307ddaa86a9ed7258ed98656b8cfb0a0137e4f58 Mon Sep 17 00:00:00 2001 From: Remy DeCausemaker Date: Thu, 25 Jun 2020 15:20:19 -0400 Subject: [PATCH 08/47] Patch 3: Fixes #1323 (#1461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated FAQ with correct link “Will Spotify’s internal plugins be open sourced, too?” had an incorrect link, and I added the correct one. * Update FAQ.md * Update FAQ.md * Fix links to include full path to .md files Co-authored-by: Nikki Beesetti <12538017+nikkibeesetti@users.noreply.github.com> Co-authored-by: Stefan Ålund --- docs/FAQ.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index e38fa84e73..7bfdbec478 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -36,7 +36,7 @@ more, read our blog post, Yes, we've already started releasing open source versions of some of the plugins we use here, and we'll continue to do so. -[Plugins](https://github.com/spotify/faq#what-is-a-plugin-in-backstage) are the +[Plugins](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage) are the building blocks of functionality in Backstage. We have over 120 plugins inside Spotify — many of those are specialized for our use, so will remain internal and proprietary to us. But we estimate that about a third of our existing plugins @@ -122,15 +122,7 @@ can browse and search for all available plugins. ​ ### Which plugin is used the most at Spotify? -​ By far, our most-used plugin is our TechDocs plugin, which we use for creating -technical documentation. Our philosophy at Spotify is to treat "docs like code", -where you write documentation using the same workflow as you write your code. -This makes it easier to create, find, and update documentation. We hope to -release -[the open source version](https://github.com/spotify/backstage/issues/687) in -the future. (See also: -"[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/faq#will-spotifys-internal-plugins-be-open-sourced-too)" -above) ​ +By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#will-spotifys-internal-plugins-be-open-sourced-too)" above) ### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? From f29b9017baaa77152f3ae5b439e8bc8b9acc5f79 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Thu, 25 Jun 2020 21:21:04 +0200 Subject: [PATCH 09/47] fixing doc links (#1456) --- docs/auth/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/auth/README.md b/docs/auth/README.md index 081d10add6..9809f57168 100644 --- a/docs/auth/README.md +++ b/docs/auth/README.md @@ -80,7 +80,7 @@ sign-in methods. More details are provided in dedicated sections of the documentation. -- [OAuth](./oauth): Description of the generic OAuth flow implemented by the +- [OAuth](./oauth.md): Description of the generic OAuth flow implemented by the [auth-backend](../../plugins/auth-backend). -- [Glossary](./glossary): Glossary of some common terms related to the auth +- [Glossary](./glossary.md): Glossary of some common terms related to the auth flows. From 3b16c5f16d688f6efec3ac096f90c53c0fac6684 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2020 21:22:00 +0200 Subject: [PATCH 10/47] build(deps): bump helmet from 3.22.0 to 3.23.2 (#1450) Bumps [helmet](https://github.com/helmetjs/helmet) from 3.22.0 to 3.23.2. - [Release notes](https://github.com/helmetjs/helmet/releases) - [Changelog](https://github.com/helmetjs/helmet/blob/master/CHANGELOG.md) - [Commits](https://github.com/helmetjs/helmet/compare/v3.22.0...v3.23.2) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/yarn.lock b/yarn.lock index 18be8c3246..14f9079fa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7663,11 +7663,6 @@ dns-packet@^1.3.1: ip "^1.1.0" safe-buffer "^5.0.1" -dns-prefetch-control@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz#73988161841f3dcc81f47686d539a2c702c88624" - integrity sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q== - dns-txt@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" @@ -9997,12 +9992,11 @@ helmet-csp@2.10.0: dasherize "2.0.0" helmet@^3.22.0: - version "3.22.0" - resolved "https://registry.npmjs.org/helmet/-/helmet-3.22.0.tgz#3a6f11d931799145f0aff15dbc563cff9e13131f" - integrity sha512-Xrqicn2nm1ZIUxP3YGuTBmbDL04neKsIT583Sjh0FkiwKDXYCMUqGqC88w3NUvVXtA75JyR2Jn6jw6ZEMOD+ZA== + version "3.23.2" + resolved "https://registry.npmjs.org/helmet/-/helmet-3.23.2.tgz#390bb6f3f5e593f90f441bdd91000912c543a898" + integrity sha512-pe0UiHw3aHbP8Lon9McCq4AN2XLUMSbhwxJnUY6U2t8wTda7F1SsYg0/pBa1BPugaRqAtx9e1/FyF6E9PsUU5A== dependencies: depd "2.0.0" - dns-prefetch-control "0.2.0" dont-sniff-mimetype "1.1.0" expect-ct "0.2.0" feature-policy "0.3.0" @@ -10012,7 +10006,6 @@ helmet@^3.22.0: hide-powered-by "1.1.0" hpkp "2.0.0" hsts "2.2.0" - ienoopen "1.1.0" nocache "2.1.0" referrer-policy "1.2.0" x-xss-protection "1.3.0" @@ -10375,11 +10368,6 @@ ieee754@^1.1.4: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== -ienoopen@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974" - integrity sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ== - iferr@^0.1.5: version "0.1.5" resolved "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" From 33870a5283af95f1aa6771ccdcc8b782c140e5a9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Jun 2020 21:18:54 +0200 Subject: [PATCH 11/47] yarnrc: bump network timeout --- .yarnrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.yarnrc b/.yarnrc index 524dcb98eb..f465c5c5e6 100644 --- a/.yarnrc +++ b/.yarnrc @@ -5,3 +5,4 @@ registry "https://registry.npmjs.org/" lastUpdateCheck 1580389148099 yarn-path ".yarn/releases/yarn-1.22.1.js" +network-timeout 600000 From 6189b5d5b924d1a4f64802febfacbda1a98e37f7 Mon Sep 17 00:00:00 2001 From: Jeff Feng <46946747+fengypants@users.noreply.github.com> Date: Fri, 26 Jun 2020 03:09:10 -0400 Subject: [PATCH 12/47] Update FAQ.md (#1464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update FAQ.md — Changed anchor links to relative links (/docs/FAQ.md#heading-name) — Restored the heading + answer for "What is a "plugin" in Backstage?" — Cleaned up extra spaces throughout and added line breaks to the answer for "What is the end-to-end user flow?". * Update FAQ.md --- docs/FAQ.md | 98 ++++++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 7bfdbec478..7d568a840e 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -15,11 +15,11 @@ No, but it can be! Backstage is designed to be a developer portal for all your infrastructure tooling, services, and documentation. So, it's not a monitoring platform — but that doesn't mean you can't integrate a monitoring tool into Backstage by writing -[a plugin](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage). +[a plugin](/docs/FAQ.md#what-is-a-plugin-in-backstage). ### How is Backstage licensed? -Backstage was released as open sourced software by Spotify and is licensed under +Backstage was released as open source software by Spotify and is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). ### Why did we open source Backstage? @@ -36,16 +36,16 @@ more, read our blog post, Yes, we've already started releasing open source versions of some of the plugins we use here, and we'll continue to do so. -[Plugins](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage) are the +[Plugins](/docs/FAQ.md#what-is-a-plugin-in-backstage) are the building blocks of functionality in Backstage. We have over 120 plugins inside Spotify — many of those are specialized for our use, so will remain internal and proprietary to us. But we estimate that about a third of our existing plugins make good open source candidates. (And we'll probably end up writing some brand -new ones, too.) ​ +new ones, too.) ### What's the roadmap for Backstage? -​ We envision three phases, which you can learn about in +We envision three phases, which you can learn about in [our project roadmap](https://github.com/spotify/backstage#project-roadmap). Even though the open source version of Backstage is relatively new compared to our internal version, we have already begun work on various aspects of all three @@ -80,134 +80,138 @@ knew that we would like to use. It strikes a good balance between power, customizability, and ease of use. A core focus of Backstage is to make plugin developers productive with as few hurdles as possible. Material-UI lets plugin makers get going easily with both -well-known tech and a large flora of components. ​ +well-known tech and a large flora of components. ### What technology does Backstage use? -​ The code base is a large-scale React application that uses TypeScript. For +The code base is a large-scale React application that uses TypeScript. For [Phase 2](https://github.com/spotify/backstage#project-roadmap), we plan to use -Node.js and GraphQL. ​ +Node.js and GraphQL. ### What is the end-to-end user flow? The happy path story. -​ There are three main user profiles for Backstage: the integrator, the -contributor, and the software engineer. ​ The **integrator** hosts the Backstage -app and configures which plugins are available to use in the app. ​ The -**contributor** adds functionality to the app by writing plugins. ​ The -**software engineer** uses the app's functionality and interacts with its -plugins. ​ +There are three main user profiles for Backstage: the integrator, the +contributor, and the software engineer. -### What is the use of a "plugin" in Backstage? +The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. -​ A Backstage Plugin adds functionality to Backstage. ​ +The **contributor** adds functionality to the app by writing plugins. +The **software engineer** uses the app's functionality and interacts with its plugins. + +### What is a "plugin" in Backstage? + +Plugins are what provide the feature functionality in Backstage. They are used to integrate different systems into Backstage's frontend, so that the developer gets a consistent UX, no matter what tool or service is being accessed on the other side. + +Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform APIs and reusable UI components. Plugins can fetch data either from the backend or an API exposed through the proxy. + +Learn more about [the different components](https://github.com/spotify/backstage#overview) that make up Backstage. ### Do I have to write plugins in TypeScript? -​ No, you can use JavaScript if you prefer. ​ We want to keep the Backstage core -APIs in TypeScript, but aren't forcing it on individual plugins. ​ +No, you can use JavaScript if you prefer. We want to keep the Backstage core +APIs in TypeScript, but aren't forcing it on individual plugins. ### How do I find out if a plugin already exists? -​ Before you write a plugin, +Before you write a plugin, [search the plugin issues](https://github.com/spotify/backstage/issues?q=is%3Aissue+label%3Aplugin+) to see if it already exists or is in the works. If no one's thought of it yet, great! Open a new issue as [a plugin suggestion](https://github.com/spotify/backstage/issues/new/choose) and describe what your plugin will do. This will help coordinate our -contributors' efforts and avoid duplicating existing functionality. ​ In the +contributors' efforts and avoid duplicating existing functionality. In the future, we will create [a plugin gallery](https://github.com/spotify/backstage/issues/260) where people -can browse and search for all available plugins. ​ +can browse and search for all available plugins. ### Which plugin is used the most at Spotify? -By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#will-spotifys-internal-plugins-be-open-sourced-too)" above) +By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](/docs/FAQ.md#will-spotifys-internal-plugins-be-open-sourced-too)" above) ### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? -​ Contributors can add open source plugins to the plugins directory in +Contributors can add open source plugins to the plugins directory in [this monorepo](https://github.com/spotify/backstage). Integrators can then configure which open source plugins are available to use in their instance of the app. Open source plugins are downloaded as npm packages published in the -open source repository. ​ While we encourage using the open source model, we +open source repository. While we encourage using the open source model, we know there are cases where contributors might want to experiment internally or keep their plugins closed source. Contributors writing closed source plugins should develop them in the plugins directory in their own Backstage repository. -Integrators also configure closed source plugins locally from the monorepo. ​ +Integrators also configure closed source plugins locally from the monorepo. ### Any plans for integrating with other repository managers, such as GitLab or Bitbucket? -​ We chose GitHub because it is the tool that we are most familiar with, so that +We chose GitHub because it is the tool that we are most familiar with, so that will naturally lead to integrations for GitHub being developed at an early -stage. ​ Hosting this project on GitHub does not exclude integrations with +stage. Hosting this project on GitHub does not exclude integrations with alternatives, such as [GitLab](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+GitLab) or Bitbucket. We believe that in time there will be plugins that will provide -functionality for these tools as well. Hopefully, contributed by the community! -​ Also note, implementations of Backstage can be hosted wherever you feel suits -your needs best. ​ +functionality for these tools as well. Hopefully, contributed by the community! Also note, implementations of Backstage can be hosted wherever you feel suits +your needs best. ### Who maintains Backstage? -​ Spotify will maintain the open source core, but we envision different parts of +Spotify will maintain the open source core, but we envision different parts of the project being maintained by various companies and contributors. We also envision a large, diverse ecosystem of open source plugins, which would be -maintained by their original authors/contributors or by the community. ​ When it +maintained by their original authors/contributors or by the community. When it comes to [deployment](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md), the system integrator (typically, the infrastructure team in your organization) -maintains Backstage in your own environment. ​ +maintains Backstage in your own environment. ### Does Spotify provide a managed version of Backstage? -​ No, this is not a service offering. We build the piece of software, and +No, this is not a service offering. We build the piece of software, and someone in your infrastructure team is responsible for [deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and -maintaining it. ​ +maintaining it. ### How secure is Backstage? -​ We take security seriously. When it comes to packages and code we scan our +We take security seriously. When it comes to packages and code we scan our repositories periodically and update our packages to the latest versions. When it comes to deployment of Backstage within an organisation it depends on the deployment and security setup in your organisation. Reach out to us on [Discord](https://discord.gg/MUpMjP2) if you have specific queries. Please report sensitive security issues via Spotify's -[bug-bounty program](https://hackerone.com/spotify) rather than GitHub. ​ +[bug-bounty program](https://hackerone.com/spotify) rather than GitHub. ### Does Backstage collect any information that is shared with Spotify? -​ No. Backstage does not collect any telemetry from any third party using the +No. Backstage does not collect any telemetry from any third party using the platform. Spotify, and the open source community, does have access to [GitHub Insights](https://github.com/features/insights), which contains -information such as contributors, commits, traffic, and dependencies. ​ +information such as contributors, commits, traffic, and dependencies. Backstage is an open platform, but you are in control of your own data. You control who has access to any data you provide to your version of Backstage and -who that data is shared with. ​ +who that data is shared with. ### Can Backstage be used to build something other than a developer portal? -​ Yes. The core frontend framework could be used for building any large-scale +Yes. The core frontend framework could be used for building any large-scale web application where (1) multiple teams are building separate parts of the app, -and (2) you want the overall experience to be consistent. ​ That being said, in +and (2) you want the overall experience to be consistent. That being said, in [Phase 2](https://github.com/spotify/backstage#project-roadmap) of the project we will add features that are needed for developer portals and systems for -managing software ecosystems. Our ambition will be to keep Backstage modular. ​ +managing software ecosystems. Our ambition will be to keep Backstage modular. ### How can I get involved? -​ Jump right in! Come help us fix some of the +Jump right in! Come help us fix some of the [early bugs and first issues](https://github.com/spotify/backstage/labels/good%20first%20issue) or reach [a new milestone](https://github.com/spotify/backstage/milestones). Or write an open source plugin for Backstage, like this [Lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse). -​ See all the ways you can +See all the ways you can [contribute here](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md). -We'd love to have you as part of the community. ​ +We'd love to have you as part of the community. ### Can I join the Backstage team? -​ If you're interested in being part of the Backstage team, reach out to +If you're interested in being part of the Backstage team, reach out to [fossopportunities@spotify.com](mailto:fossopportunities@spotify.com) From 7957e154636898fa50974a23e85bc05cda108b22 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2020 09:09:49 +0200 Subject: [PATCH 13/47] build(deps): bump inquirer from 7.1.0 to 7.2.0 (#1451) Bumps [inquirer](https://github.com/SBoudrias/Inquirer.js) from 7.1.0 to 7.2.0. - [Release notes](https://github.com/SBoudrias/Inquirer.js/releases) - [Commits](https://github.com/SBoudrias/Inquirer.js/compare/inquirer@7.1.0...inquirer@7.2.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index 14f9079fa5..0b64260c92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10615,9 +10615,9 @@ inquirer@^6.2.0: through "^2.3.6" inquirer@^7.0.0, inquirer@^7.0.4: - version "7.1.0" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" - integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== + version "7.2.0" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.2.0.tgz#63ce99d823090de7eb420e4bb05e6f3449aa389a" + integrity sha512-E0c4rPwr9ByePfNlTIB8z51kK1s2n6jrHuJeEHENl/sbq2G/S1auvibgEwNR4uSyiU+PiYHqSwsgGiXjG8p5ZQ== dependencies: ansi-escapes "^4.2.1" chalk "^3.0.0" @@ -16650,20 +16650,13 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.3.3, rxjs@^6.5.5: +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.3, rxjs@^6.5.4, rxjs@^6.5.5: version "6.5.5" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== dependencies: tslib "^1.9.0" -rxjs@^6.4.0, rxjs@^6.5.3, rxjs@^6.5.4: - version "6.5.4" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" - integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== - dependencies: - tslib "^1.9.0" - safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" From 891b5e000e4bc1501fe7a1a66dd0dee035c4f9fc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2020 09:12:39 +0200 Subject: [PATCH 14/47] build(deps-dev): bump cypress from 4.7.0 to 4.9.0 (#1454) Bumps [cypress](https://github.com/cypress-io/cypress) from 4.7.0 to 4.9.0. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Commits](https://github.com/cypress-io/cypress/compare/v4.7.0...v4.9.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 92 ++++++++++++++++++------------------------------------- 1 file changed, 30 insertions(+), 62 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0b64260c92..713a46b837 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4762,10 +4762,10 @@ aproba@^2.0.0: resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== -arch@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" - integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== +arch@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" + integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== are-we-there-yet@~1.1.2: version "1.1.5" @@ -5002,7 +5002,7 @@ async@^2.6.1, async@^2.6.2: dependencies: lodash "^4.17.14" -async@^3.1.0: +async@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== @@ -6428,21 +6428,16 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== -commander@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" - integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== +commander@4.1.1, commander@^4.0.0, commander@^4.0.1, commander@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^4.0.0, commander@^4.0.1, commander@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - commander@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" @@ -7184,38 +7179,38 @@ cyclist@^1.0.1: integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= cypress@*, cypress@^4.2.0: - version "4.7.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-4.7.0.tgz#3ea29bddaf9a1faeaa5b8d54b60a84ed1cafa83d" - integrity sha512-Vav6wUFhPRlImIND/2lOQlUnAWzgCC/iXyJlJjX9nJOJul5LC1vUpf/m8Oiae870PFPyT0ZLLwPHKTXZNdXmHw== + version "4.9.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-4.9.0.tgz#c188a3864ddf841c0fdc81a9e4eff5cf539cd1c1" + integrity sha512-qGxT5E0j21FPryzhb0OBjCdhoR/n1jXtumpFFSBPYWsaZZhNaBvc3XlBUDEZKkkXPsqUFYiyhWdHN/zo0t5FcA== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" "@types/sinonjs__fake-timers" "6.0.1" "@types/sizzle" "2.3.2" - arch "2.1.1" + arch "2.1.2" bluebird "3.7.2" cachedir "2.3.0" chalk "2.4.2" check-more-types "2.24.0" cli-table3 "0.5.1" - commander "4.1.0" + commander "4.1.1" common-tags "1.8.0" debug "4.1.1" - eventemitter2 "4.1.2" + eventemitter2 "6.4.2" execa "1.0.0" executable "4.1.1" extract-zip "1.7.0" fs-extra "8.1.0" - getos "3.1.4" + getos "3.2.1" is-ci "2.0.0" - is-installed-globally "0.1.0" + is-installed-globally "0.3.2" lazy-ass "1.6.0" listr "0.14.3" lodash "4.17.15" log-symbols "3.0.0" minimist "1.2.5" - moment "2.24.0" + moment "2.26.0" ospath "1.2.2" pretty-bytes "5.3.0" ramda "0.26.1" @@ -8445,10 +8440,10 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" -eventemitter2@4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15" - integrity sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU= +eventemitter2@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970" + integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw== eventemitter3@^3.1.0: version "3.1.2" @@ -9411,12 +9406,12 @@ getopts@2.2.5: resolved "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== -getos@3.1.4: - version "3.1.4" - resolved "https://registry.npmjs.org/getos/-/getos-3.1.4.tgz#29cdf240ed10a70c049add7b6f8cb08c81876faf" - integrity sha512-UORPzguEB/7UG5hqiZai8f0vQ7hzynMQyJLxStoQ8dPGAcmgsfXOPA4iE/fGtweHYkK+z4zc9V0g+CIFRf5HYw== +getos@3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== dependencies: - async "^3.1.0" + async "^3.2.0" getpass@^0.1.1: version "0.1.7" @@ -9521,13 +9516,6 @@ glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glo once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" - integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= - dependencies: - ini "^1.3.4" - global-dirs@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" @@ -10930,15 +10918,7 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= -is-installed-globally@0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" - integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= - dependencies: - global-dirs "^0.1.0" - is-path-inside "^1.0.0" - -is-installed-globally@^0.3.1: +is-installed-globally@0.3.2, is-installed-globally@^0.3.1: version "0.3.2" resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== @@ -11012,13 +10992,6 @@ is-path-in-cwd@^2.0.0: dependencies: is-path-inside "^2.1.0" -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" - is-path-inside@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" @@ -13144,12 +13117,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@2.24.0: - version "2.24.0" - resolved "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== - -moment@^2.25.3, moment@^2.26.0: +moment@2.26.0, moment@^2.25.3, moment@^2.26.0: version "2.26.0" resolved "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== @@ -14389,7 +14357,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-is-inside@^1.0.1, path-is-inside@^1.0.2: +path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= From c52c1e7a8eabb8121e0ff5852502b1c2e22e8915 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2020 09:13:24 +0200 Subject: [PATCH 15/47] build(deps): bump graphiql from 1.0.0-alpha.10 to 1.0.3 (#1453) Bumps [graphiql](https://github.com/graphql/graphiql) from 1.0.0-alpha.10 to 1.0.3. - [Release notes](https://github.com/graphql/graphiql/releases) - [Changelog](https://github.com/graphql/graphiql/blob/main/CHANGELOG.md) - [Commits](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.10...graphiql@1.0.3) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 355 +++++++++++------------------------------------------- 1 file changed, 71 insertions(+), 284 deletions(-) diff --git a/yarn.lock b/yarn.lock index 713a46b837..d1dbcf77ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1184,71 +1184,6 @@ unique-filename "^1.1.1" which "^1.3.1" -"@graphql-toolkit/common@0.10.7", "@graphql-toolkit/common@~0.10.2": - version "0.10.7" - resolved "https://registry.npmjs.org/@graphql-toolkit/common/-/common-0.10.7.tgz#e5d4ddeae080c4f7357bc7d6a8d02e75578e9bd1" - integrity sha512-epcJvmIAo+vSEY76F0Dj1Ef6oeewT5pdMe1obHj7LHXN9V22O86aQzwdEEm1iG91qROqSw/apcDnSCMjuVeQVA== - dependencies: - aggregate-error "3.0.1" - camel-case "4.1.1" - graphql-tools "5.0.0" - lodash "4.17.15" - -"@graphql-toolkit/core@~0.10.2": - version "0.10.7" - resolved "https://registry.npmjs.org/@graphql-toolkit/core/-/core-0.10.7.tgz#e4d86d9e439fbb05b634054b0865c16d488fad97" - integrity sha512-LXcFLG7XcRJrPz/xD+0cExzLx/ptVynDM20650/FbmHbKOU50d9mSbcsrzAOq/3f4q3HrRDssvn0f6pPm0EHMg== - dependencies: - "@graphql-toolkit/common" "0.10.7" - "@graphql-toolkit/schema-merging" "0.10.7" - aggregate-error "3.0.1" - globby "11.0.0" - import-from "^3.0.0" - is-glob "4.0.1" - lodash "4.17.15" - p-limit "2.3.0" - resolve-from "5.0.0" - tslib "1.11.2" - unixify "1.0.0" - valid-url "1.0.9" - -"@graphql-toolkit/graphql-file-loader@~0.10.2": - version "0.10.7" - resolved "https://registry.npmjs.org/@graphql-toolkit/graphql-file-loader/-/graphql-file-loader-0.10.7.tgz#496e49712def12449b339739d3583ed1bda7a24f" - integrity sha512-6tUIuw/YBlm0VyVgXgMrOXsEQ+WpXVgr2NQwHNzmZo82kPGqImveq7A2D3gBWLyVTcinDScRcKJMxM4kCF5T0A== - dependencies: - "@graphql-toolkit/common" "0.10.7" - tslib "1.11.2" - -"@graphql-toolkit/json-file-loader@~0.10.2": - version "0.10.7" - resolved "https://registry.npmjs.org/@graphql-toolkit/json-file-loader/-/json-file-loader-0.10.7.tgz#6ac38258bc3da8540b38232b71bb5820bf02cff6" - integrity sha512-nVISrODqvn5LiQ4nKL5pz1Let/W1tuj2viEwrNyTS+9mcjaCE2nhV5MOK/7ZY0cR+XeA4N2u65EH1lQd63U3Cw== - dependencies: - "@graphql-toolkit/common" "0.10.7" - tslib "1.11.2" - -"@graphql-toolkit/schema-merging@0.10.7", "@graphql-toolkit/schema-merging@~0.10.2": - version "0.10.7" - resolved "https://registry.npmjs.org/@graphql-toolkit/schema-merging/-/schema-merging-0.10.7.tgz#7b33becd48629bc656d602405c0b5c17d33ebd85" - integrity sha512-VngxJbVdRfXYhdMLhL90pqN+hD/2XTZwhHPGvpWqmGQhT6roc98yN3xyDyrWFYYsuiY4gTexdmrHQ3d7mzitwA== - dependencies: - "@graphql-toolkit/common" "0.10.7" - deepmerge "4.2.2" - graphql-tools "5.0.0" - tslib "1.11.2" - -"@graphql-toolkit/url-loader@~0.10.2": - version "0.10.7" - resolved "https://registry.npmjs.org/@graphql-toolkit/url-loader/-/url-loader-0.10.7.tgz#0de998eeeaebcbf918526a26cb5f30f8250211f0" - integrity sha512-Ec3T4Zuo63LwG+RfK2ryz8ChPfncBf8fiSJ1xr68FtLDVznDNulvlNKFbfREE5koWejwsnJrjLCv6IX5IbhExg== - dependencies: - "@graphql-toolkit/common" "0.10.7" - cross-fetch "3.0.4" - graphql-tools "5.0.0" - tslib "1.11.2" - valid-url "1.0.9" - "@hapi/address@^2.1.2": version "2.1.4" resolved "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -4416,13 +4351,6 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" -"@wry/equality@^0.1.2": - version "0.1.11" - resolved "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790" - integrity sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA== - dependencies: - tslib "^1.9.3" - "@xobotyi/scrollbar-width@1.9.5": version "1.9.5" resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" @@ -4550,7 +4478,7 @@ agentkeepalive@^3.4.1: dependencies: humanize-ms "^1.2.1" -aggregate-error@3.0.1, aggregate-error@^3.0.0: +aggregate-error@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== @@ -4708,45 +4636,6 @@ anymatch@^3.0.3, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -apollo-link-http-common@^0.2.14: - version "0.2.16" - resolved "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz#756749dafc732792c8ca0923f9a40564b7c59ecc" - integrity sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg== - dependencies: - apollo-link "^1.2.14" - ts-invariant "^0.4.0" - tslib "^1.9.3" - -apollo-link@^1.2.12, apollo-link@^1.2.14: - version "1.2.14" - resolved "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9" - integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg== - dependencies: - apollo-utilities "^1.3.0" - ts-invariant "^0.4.0" - tslib "^1.9.3" - zen-observable-ts "^0.8.21" - -apollo-upload-client@^13.0.0: - version "13.0.0" - resolved "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-13.0.0.tgz#146d1ddd85d711fcac8ca97a72d3ca6787f2b71b" - integrity sha512-lJ9/bk1BH1lD15WhWRha2J3+LrXrPIX5LP5EwiOUHv8PCORp4EUrcujrA3rI5hZeZygrTX8bshcuMdpqpSrvtA== - dependencies: - "@babel/runtime" "^7.9.2" - apollo-link "^1.2.12" - apollo-link-http-common "^0.2.14" - extract-files "^8.0.0" - -apollo-utilities@^1.3.0: - version "1.3.4" - resolved "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz#6129e438e8be201b6c55b0f13ce49d2c7175c9cf" - integrity sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig== - dependencies: - "@wry/equality" "^0.1.2" - fast-json-stable-stringify "^2.0.0" - ts-invariant "^0.4.0" - tslib "^1.10.0" - app-root-dir@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz#38187ec2dea7577fff033ffcb12172692ff6e118" @@ -5905,7 +5794,7 @@ callsites@^3.0.0: resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camel-case@4.1.1, camel-case@^4.1.1: +camel-case@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz#1fc41c854f00e2f7d0139dfeba1542d6896fe547" integrity sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q== @@ -6306,13 +6195,13 @@ code-point-at@^1.0.0: resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemirror-graphql@^0.12.0-alpha.8: - version "0.12.0-alpha.8" - resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.12.0-alpha.8.tgz#cd7653299baefcdff2349bc9c3def9549c78775c" - integrity sha512-leCFSjrwtdKQE70ROWIUoc1U6F8fy6i+cd9/0D1dRbEecoleCt5+zODdANETw9map50kxNPf3EcGW4BPYj14hw== +codemirror-graphql@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.12.0.tgz#55f782ffaaea7ff8dbda037e6b97b584dff2402a" + integrity sha512-C/0vKzQT5Uw+DLz6/MwKer29FQM52sh9Bem+VdDW42094j8nES1sdnuqj4k5ahNdQpW4FmVeoj/ngn2g3AWmgg== dependencies: - graphql-language-service-interface "^2.4.0-alpha.8" - graphql-language-service-parser "^1.6.0-alpha.4" + graphql-language-service-interface "^2.4.0" + graphql-language-service-parser "^1.6.0" codemirror@^5.52.2: version "5.53.2" @@ -6771,17 +6660,6 @@ cors@^2.8.5: object-assign "^4" vary "^1" -cosmiconfig@6.0.0, cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - cosmiconfig@^5.0.0, cosmiconfig@^5.1.0, cosmiconfig@^5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -6792,6 +6670,17 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.1.0, cosmiconfig@^5.2.1: js-yaml "^3.13.1" parse-json "^4.0.0" +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + create-ecdh@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" @@ -6838,7 +6727,7 @@ cross-env@^7.0.0: dependencies: cross-spawn "^7.0.1" -cross-fetch@3.0.4, cross-fetch@^3.0.4: +cross-fetch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.4.tgz#7bef7020207e684a7638ef5f2f698e24d9eb283c" integrity sha512-MSHgpjQqgbT/94D4CyADeNoYh52zMkCX4pcJvPP5WqPsLFMKjr2TCMg381ox5qI0ii2dPwaLx/00477knXqXVw== @@ -7412,7 +7301,7 @@ deep-object-diff@^1.1.0: resolved "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== -deepmerge@4.2.2, deepmerge@^4.2.2: +deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== @@ -7518,11 +7407,6 @@ depd@~1.1.2: resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -deprecated-decorator@^0.1.6: - version "0.1.6" - resolved "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" - integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc= - deprecation@^2.0.0, deprecation@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" @@ -8666,11 +8550,6 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-files@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz#46a0690d0fe77411a2e3804852adeaa65cd59288" - integrity sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ== - extract-zip@1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" @@ -9586,18 +9465,6 @@ globalthis@^1.0.0: dependencies: define-properties "^1.1.3" -globby@11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz#56fd0e9f0d4f8fb0c456f1ab0dee96e1380bc154" - integrity sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - globby@8.0.2: version "8.0.2" resolved "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" @@ -9709,86 +9576,55 @@ graceful-fs@^4.2.4: integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== graphiql@^1.0.0-alpha.10: - version "1.0.0-alpha.10" - resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.0.0-alpha.10.tgz#9b6b5e554c6586e9bc09ebc863df8bcfd844110d" - integrity sha512-fKyYXQQRjsxOQw0rZMvICA2+P7ieUjj8AyfMaYlQeXGsGkZo0FUvBXVsN6LTraAPRBvVFo2jH9MSH68ujDXoeQ== + version "1.0.3" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.0.3.tgz#f6b5d5c417d8f1a28786d3228a69883426ba74ad" + integrity sha512-6YW76m2exdpGiVS75I0QsbM4NanvenKFR9FtW/IFbui0mWv89iVa1FbdjhUt+kxa+LX/WlqTshiqiZRz7E5Bgg== dependencies: "@emotion/core" "^10.0.28" "@mdx-js/react" "^1.5.2" codemirror "^5.52.2" - codemirror-graphql "^0.12.0-alpha.8" + codemirror-graphql "^0.12.0" copy-to-clipboard "^3.2.0" entities "^2.0.0" markdown-it "^10.0.0" - monaco-graphql "^0.1.1" + monaco-graphql "^0.2.0" regenerator-runtime "^0.13.5" theme-ui "^0.3.1" -graphql-config@3.0.0-rc.2: - version "3.0.0-rc.2" - resolved "https://registry.npmjs.org/graphql-config/-/graphql-config-3.0.0-rc.2.tgz#760a1d7bcf8b114b7c1675c5a6ac624976d62e09" - integrity sha512-fb14mSDRatKsz2XXWgQjupbRsur/5vfQqHFFKo+N7hZNzRdT4hm+52HG8gn57mHQNZsN18q4aROwRQQRICMqZA== +graphql-language-service-interface@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.4.0.tgz#4e2e63242c76197c4f56c61122db68187ea566b3" + integrity sha512-r7DQPyhCFY5TlpEukdh9tekJ9hAc7MD9TdOsb5CfAPlsIb1/faVVo2Ty19PxGSYDxygXjwpKLOQD0LqqFuw63A== dependencies: - "@graphql-toolkit/common" "~0.10.2" - "@graphql-toolkit/core" "~0.10.2" - "@graphql-toolkit/graphql-file-loader" "~0.10.2" - "@graphql-toolkit/json-file-loader" "~0.10.2" - "@graphql-toolkit/schema-merging" "~0.10.2" - "@graphql-toolkit/url-loader" "~0.10.2" - cosmiconfig "6.0.0" - globby "11.0.0" - minimatch "3.0.4" - tslib "^1.11.1" - -graphql-language-service-interface@^2.4.0-alpha.8: - version "2.4.0-alpha.8" - resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.4.0-alpha.8.tgz#2f50b107c72be01796417bcfec2669d42f3b57c7" - integrity sha512-HT77H0GQk2jtpE9NAE+f8aNKvCkv5e7cZ32RZd+lCL23qNmuLJYpQcy+pbuRxxFKOWpiZLa5lc17T010gd64rQ== - dependencies: - graphql-language-service-parser "^1.6.0-alpha.4" - graphql-language-service-types "^1.6.0-alpha.6" - graphql-language-service-utils "^2.4.0-alpha.7" + graphql-language-service-parser "^1.6.0" + graphql-language-service-types "^1.6.0" + graphql-language-service-utils "^2.4.0" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@^1.6.0-alpha.4: - version "1.6.0-alpha.4" - resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.6.0-alpha.4.tgz#5d80046b1a3848a655d8f514ef65f40261b74897" - integrity sha512-l76uie88SNsa37Noc0/Bpw3HT1UxLdIrrs5A3NDj6Fm4FHmri+0Mvgjygpa3y4Y0W3AGje2MILtuJUmUSjiriA== +graphql-language-service-parser@^1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.6.0.tgz#53a97619461ed41ae237b9070e7e20cfaca56118" + integrity sha512-tkfYXl6WBECWNcsyw7O09c0oCMrxqr3JGyDNvdISDSDhvk8EwEk+AOweBEJkycJwoiv1lVlM+EdLfj7dzw4/6w== -graphql-language-service-types@^1.6.0-alpha.6: - version "1.6.0-alpha.6" - resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.6.0-alpha.6.tgz#a7a50abbdfaf41074019d107754fc9c00ed824e2" - integrity sha512-cQfLC4CYDBVKcr3C53apqqd9imT+el3oEvIPz5/MocClWAZa4EsoEr2Z4WP9X5L4QrLk3hhFOJHor1aPzQRsLw== +graphql-language-service-types@^1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.6.0.tgz#32529bb9d2e3b8ae9dd6d9646e15481ac5dfe9ad" + integrity sha512-7y4pkhTL+MtarC8CeHpvRYVc6pkT6ITdQXs+Gr7OClnk4Lz5nQEK0eO29kUdd0jEnYfHyh5iqhnL5Owfy+wT0A== -graphql-language-service-utils@^2.4.0-alpha.7: - version "2.4.0-alpha.7" - resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.4.0-alpha.7.tgz#a78150eb57a079af52eaffadf9f21a9b3c82cea9" - integrity sha512-iPw9aFUWAJJx+aWY9DUf4rvIKnJVB1Qoaf7NzHegEC7LMIg7052i8ealMFKiSqk0q/zP6PCmiQblCMnGCjnEwg== +graphql-language-service-utils@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.4.0.tgz#d4992a1968860abc43a118de498fec84361a4b87" + integrity sha512-aLa+8iqb7AJYgdcawsKH8/KLc14DHcRsnveshOm8hN6bBVT0YiQP6mEfJoci741O74uaDF2zj1J3c02vorichw== dependencies: - graphql-language-service-types "^1.6.0-alpha.6" + graphql-language-service-types "^1.6.0" -graphql-language-service@^3.0.0-alpha.1: - version "3.0.0-alpha.1" - resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-3.0.0-alpha.1.tgz#dc04c0ca3f35ce322dc38548aaa023cde867ca1b" - integrity sha512-JkkONXR7+yYcm8Q4bohBJtRl6hLodgogt01lS0ROX8lcwkaihKaU+dyoSVgrRaNAibahjFrDLo0j7iAOarJ5ow== +graphql-language-service@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-3.0.0.tgz#953c3bbf4cc35ff9a7bd54e428bca17832367490" + integrity sha512-ySPrbecxtjQtzIFdCIOoSJzaco9dQvZK+wZN/tTjcSEEWh3fgMYXmctAG+WddJiZg3AB7I4Xsb2AsO5yFBbLHg== dependencies: - graphql-config "3.0.0-rc.2" - graphql-language-service-interface "^2.4.0-alpha.8" - graphql-language-service-types "^1.6.0-alpha.6" - -graphql-tools@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/graphql-tools/-/graphql-tools-5.0.0.tgz#67281c834a0e29f458adba8018f424816fa627e9" - integrity sha512-5zn3vtn//382b7G3Wzz3d5q/sh+f7tVrnxeuhTMTJ7pWJijNqLxH7VEzv8VwXCq19zAzHYEosFHfXiK7qzvk7w== - dependencies: - apollo-link "^1.2.14" - apollo-upload-client "^13.0.0" - deprecated-decorator "^0.1.6" - form-data "^3.0.0" - iterall "^1.3.0" - node-fetch "^2.6.0" - tslib "^1.11.1" - uuid "^7.0.3" + graphql-language-service-interface "^2.4.0" + graphql-language-service-types "^1.6.0" graphql@15.1.0, graphql@^15.0.0: version "15.1.0" @@ -10887,13 +10723,6 @@ is-generator-fn@^2.0.0: resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@4.0.1, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - is-glob@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" @@ -10908,6 +10737,13 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-hexadecimal@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" @@ -11255,11 +11091,6 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterall@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" - integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== - iterate-iterator@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6" @@ -13127,13 +12958,13 @@ monaco-editor@^0.20.0: resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea" integrity sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ== -monaco-graphql@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/monaco-graphql/-/monaco-graphql-0.1.1.tgz#d74b03a14d60805841157efe99c3ddec6e8c1407" - integrity sha512-P6K03iP2hme9geA9TR2zYkSrdhVR0/YcMBQjAaTALGvI4dIILTieGxuBeElZAZFEhsj38CNZZKUF8pKXapA86Q== +monaco-graphql@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/monaco-graphql/-/monaco-graphql-0.2.0.tgz#be6bd165b6cbdc624fde6c108fe9ee32f50f2e80" + integrity sha512-IwpbJvrNlFxq6xjnQQ8eC72ep+q2vfiUaHBNkfqe314CXYnUCcXYfoMBfR5EuLAXRBOTf4CKl6BCN1/9eHrq7A== dependencies: - graphql-language-service "^3.0.0-alpha.1" - graphql-language-service-utils "^2.4.0-alpha.7" + graphql-language-service "^3.0.0" + graphql-language-service-utils "^2.4.0" monaco-editor "^0.20.0" morgan@^1.10.0: @@ -13991,13 +13822,6 @@ p-finally@^2.0.0: resolved "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== -p-limit@2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -16354,11 +16178,6 @@ resolve-dir@^1.0.0, resolve-dir@^1.0.1: expand-tilde "^2.0.0" global-modules "^1.0.0" -resolve-from@5.0.0, resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - resolve-from@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" @@ -16369,6 +16188,11 @@ resolve-from@^4.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-pathname@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" @@ -18367,13 +18191,6 @@ ts-interface-checker@^0.1.9: resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.10.tgz#b68a49e37e90a05797e590f08494dd528bf383cf" integrity sha512-UJYuKET7ez7ry0CnvfY6fPIUIZDw+UI3qvTUQeS2MyI4TgEeWAUBqy185LeaHcdJ9zG2dgFpPJU/AecXU0Afug== -ts-invariant@^0.4.0: - version "0.4.4" - resolved "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" - integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== - dependencies: - tslib "^1.9.3" - ts-jest@^26.0.0: version "26.1.0" resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.0.tgz#e9070fc97b3ea5557a48b67c631c74eb35e15417" @@ -18432,21 +18249,11 @@ tslib@1.10.0: resolved "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tslib@1.11.2: - version "1.11.2" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz#9c79d83272c9a7aaf166f73915c9667ecdde3cc9" - integrity sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg== - tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.11.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== -tslib@^1.11.1: - version "1.13.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - tsutils@^3.17.1: version "3.17.1" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -18729,13 +18536,6 @@ universalify@^1.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== -unixify@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" - integrity sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA= - dependencies: - normalize-path "^2.1.1" - unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -18954,11 +18754,6 @@ v8flags@^3.1.3: dependencies: homedir-polyfill "^1.0.1" -valid-url@1.0.9: - version "1.0.9" - resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" - integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= - validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -19732,15 +19527,7 @@ yup@^0.29.1: synchronous-promise "^2.0.10" toposort "^2.0.2" -zen-observable-ts@^0.8.21: - version "0.8.21" - resolved "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d" - integrity sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg== - dependencies: - tslib "^1.9.3" - zen-observable "^0.8.0" - -zen-observable@^0.8.0, zen-observable@^0.8.15: +zen-observable@^0.8.15: version "0.8.15" resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== From 029c6d0b50479610eafb3fb08251259a2e9c1a94 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Fri, 26 Jun 2020 09:27:24 +0200 Subject: [PATCH 16/47] fix(scaffolder): show progressbar if no data --- plugins/scaffolder/src/components/ScaffolderPage/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index aa2a8b639a..2f9774757b 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -90,7 +90,7 @@ const ScaffolderPage: React.FC<{}> = () => { . - {isValidating && } + {!templates && isValidating && } {templates && templates.map(template => { @@ -103,7 +103,7 @@ const ScaffolderPage: React.FC<{}> = () => { }`} type={template.spec.type ?? ''} description={template.metadata.description ?? '-'} - tags={template.metadata?.tags ?? []} + tags={(template.metadata?.tags as string[]) ?? []} /> ); From a462730d30b10e8979efc81775aeb203612d84ec Mon Sep 17 00:00:00 2001 From: Nicholas Pirrello Date: Wed, 24 Jun 2020 22:55:14 -0400 Subject: [PATCH 17/47] Add okta integration --- plugins/auth-backend/package.json | 2 + .../auth-backend/src/providers/factories.ts | 2 + .../auth-backend/src/providers/okta/index.ts | 16 ++ .../src/providers/okta/provider.ts | 208 ++++++++++++++++++ .../src/providers/okta/types.d.ts | 22 ++ plugins/auth-backend/src/providers/types.ts | 4 + plugins/auth-backend/src/service/router.ts | 9 + 7 files changed, 263 insertions(+) create mode 100644 plugins/auth-backend/src/providers/okta/index.ts create mode 100644 plugins/auth-backend/src/providers/okta/provider.ts create mode 100644 plugins/auth-backend/src/providers/okta/types.d.ts diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 2de098c7eb..d137357d60 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -40,6 +40,8 @@ "passport": "^0.4.1", "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", + "passport-oauth2": "^1.5.0", + "passport-okta-oauth": "^0.0.1", "passport-saml": "^1.3.3", "uuid": "^8.0.0", "winston": "^3.2.1", diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 5096b0698a..9feefdac9f 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -18,6 +18,7 @@ import Router from 'express-promise-router'; import { createGithubProvider } from './github'; import { createGoogleProvider } from './google'; import { createSamlProvider } from './saml'; +import { createOktaProvider } from './okta'; import { AuthProviderFactory, AuthProviderConfig } from './types'; import { Logger } from 'winston'; import { TokenIssuer } from '../identity'; @@ -26,6 +27,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, github: createGithubProvider, saml: createSamlProvider, + okta: createOktaProvider, }; export const createAuthProviderRouter = ( diff --git a/plugins/auth-backend/src/providers/okta/index.ts b/plugins/auth-backend/src/providers/okta/index.ts new file mode 100644 index 0000000000..bc32601ac2 --- /dev/null +++ b/plugins/auth-backend/src/providers/okta/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 { createOktaProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts new file mode 100644 index 0000000000..7b3d5770d7 --- /dev/null +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -0,0 +1,208 @@ +/* + * 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 express, { Request } from 'express'; +import { OAuthProvider } from '../../lib/OAuthProvider'; +import { Strategy as OktaStrategy } from 'passport-okta-oauth'; +import passport from 'passport'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, + executeFetchUserProfileStrategy, +} from '../../lib/PassportStrategyHelper'; +import { + OAuthProviderHandlers, + RedirectInfo, + AuthProviderConfig, + EnvironmentProviderConfig, + OAuthProviderOptions, + OAuthProviderConfig, + OAuthResponse, + PassportDoneCallback, +} from '../types'; +import { + EnvironmentHandler, + EnvironmentHandlers, +} from '../../lib/EnvironmentHandler'; +import { Logger } from 'winston'; +import { StateStore } from 'passport-oauth2'; +import { TokenIssuer } from '../../identity'; + +type PrivateInfo = { + refreshToken: string; +}; + +export class OktaAuthProvider implements OAuthProviderHandlers { + + private readonly _strategy: any; + + /** + * Due to passport-okta-oauth forcing options.state = true, + * passport-oauth2 requires express-session to be installed + * so that the 'state' parameter of the oauth2 flow can be stored. + * This implementation of StateStore matches the NullStore found within + * passport-oauth2, which is the StateStore implementation used when options.state = false, + * allowing us to avoid using express-session in order to integrate with Okta. + */ + private _store: StateStore = { + store(req: Request, cb: any) { + cb(null, null); + }, + verify(req: Request, state: string, cb: any) { + cb(null, true); + }, + } + + constructor(options: OAuthProviderOptions) { + this._strategy = new OktaStrategy({ + passReqToCallback: false as true, + ...options, + store: this._store, + response_type: 'code', + }, ( + accessToken: any, + refreshToken: any, + params: any, + rawProfile: passport.Profile, + done: PassportDoneCallback, + ) => { + const profile = makeProfileInfo(rawProfile, params.id_token); + + done( + undefined, + { + providerInfo: { + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + profile, + }, + { + refreshToken, + }, + ) + }); + } + + async start( + req: express.Request, + options: Record + ): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } + + async handler( + req: express.Request + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { response, privateInfo } = await executeFrameHandlerStrategy< + OAuthResponse, + PrivateInfo + >(req, this._strategy); + + return { + response: await this.populateIdentity(response), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(refreshToken: string, scope: string): Promise { + const { accessToken, params } = await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); + + const profile = await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + params.id_token, + ); + + return this.populateIdentity({ + providerInfo: { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, + profile, + }); + + } + + private async populateIdentity( + response: OAuthResponse, + ): Promise { + const { profile } = response; + + if (!profile.email) { + throw new Error('Okta profile contained no email'); + } + + // TODO(Rugvip): Hardcoded to the local part of the email for now + const id = profile.email.split('@')[0]; + + return { ...response, backstageIdentity: { id } }; + } +} + +export function createOktaProvider( + { baseUrl }: AuthProviderConfig, + providerConfig: EnvironmentProviderConfig, + logger: Logger, + tokenIssuer: TokenIssuer, +) { + const envProviders: EnvironmentHandlers = {}; + + for (const [env, envConfig] of Object.entries(providerConfig)) { + const config = (envConfig as unknown) as OAuthProviderConfig; + const { secure, appOrigin } = config; + const callbackURLParam = `?env=${env}`; + const opts = { + audience: config.audience, + clientID: config.clientId, + clientSecret: config.clientSecret, + callbackURL: `${baseUrl}/okta/handler/frame${callbackURLParam}`, + }; + + if (!opts.clientID || !opts.clientSecret || !opts.audience) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars', + ); + } + + logger.warn( + 'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable', + ); + continue; + } + + envProviders[env] = new OAuthProvider(new OktaAuthProvider(opts), { + disableRefresh: false, + providerId: 'okta', + secure, + baseUrl, + appOrigin, + tokenIssuer, + }); + } + + return new EnvironmentHandler(envProviders); +} diff --git a/plugins/auth-backend/src/providers/okta/types.d.ts b/plugins/auth-backend/src/providers/okta/types.d.ts new file mode 100644 index 0000000000..bed6d24043 --- /dev/null +++ b/plugins/auth-backend/src/providers/okta/types.d.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ +declare module 'passport-okta-oauth' { + + export class Strategy { + constructor(options: any, verify: any) + } +} + \ No newline at end of file diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 56d4db79fa..aec4f9dfd0 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -55,6 +55,10 @@ export type OAuthProviderConfig = { * Client Secret of the auth provider. */ clientSecret: string; + /** + * The location of the OAuth Authorization Server + */ + audience?: string; }; export type EnvironmentProviderConfig = { diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 28718e85a1..b9c0ff4496 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -82,6 +82,15 @@ export async function createRouter( issuer: 'passport-saml', }, }, + okta: { + development: { + appOrigin: 'http://localhost:3000', + secure: false, + clientId: process.env.AUTH_OKTA_CLIENT_ID!, + clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET!, + audience: process.env.AUTH_OKTA_AUDIENCE, + } + } }, }, }; From 1ae797a9c65fe1304d8ec3d69c67cac01410bfaa Mon Sep 17 00:00:00 2001 From: Nicholas Pirrello Date: Wed, 24 Jun 2020 22:56:10 -0400 Subject: [PATCH 18/47] Add ui implementation for okta sso integration --- packages/app/src/App.tsx | 2 +- packages/app/src/apis.ts | 11 ++ .../core-api/src/apis/definitions/auth.ts | 17 ++ .../src/apis/implementations/auth/index.ts | 1 + .../auth/okta/OktaAuth.test.ts | 111 ++++++++++++ .../implementations/auth/okta/OktaAuth.ts | 168 ++++++++++++++++++ .../apis/implementations/auth/okta/index.ts | 18 ++ .../apis/implementations/auth/okta/types.ts | 27 +++ .../core/src/layout/Sidebar/UserSettings.tsx | 6 + .../src/layout/SignInPage/oktaProvider.tsx | 93 ++++++++++ .../core/src/layout/SignInPage/providers.tsx | 4 +- yarn.lock | 35 +++- 12 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts create mode 100644 packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts create mode 100644 packages/core-api/src/apis/implementations/auth/okta/index.ts create mode 100644 packages/core-api/src/apis/implementations/auth/okta/types.ts create mode 100644 packages/core/src/layout/SignInPage/oktaProvider.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index f4515cbf01..ccaeb2828b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,7 +31,7 @@ const app = createApp({ plugins: Object.values(plugins), components: { SignInPage: props => ( - + ), }, }); diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 5011b16eaf..d2b2706464 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -26,10 +26,12 @@ import { FeatureFlags, GoogleAuth, GithubAuth, + OktaAuth, oauthRequestApiRef, OAuthRequestManager, googleAuthApiRef, githubAuthApiRef, + oktaAuthApiRef, storageApiRef, WebStorage, } from '@backstage/core'; @@ -91,6 +93,15 @@ export const apis = (config: ConfigApi) => { }), ); + builder.add( + oktaAuthApiRef, + OktaAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), + ); + builder.add( techRadarApiRef, new TechRadar({ diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index d5c0e9c0a4..f82352cbc8 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -233,3 +233,20 @@ export const githubAuthApiRef = createApiRef< id: 'core.auth.github', description: 'Provides authentication towards Github APIs', }); + +/** + * Provides authentication towards Okta APIs. + * + * See https://developer.okta.com/docs/reference/api/oidc/ + * for a full list of supported scopes. + */ +export const oktaAuthApiRef = createApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionStateApi +>({ + id: 'core.auth.okta', + description: 'Provides authentication towards Okta APIs', +}); diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index f13368b5c4..4fa992dfb5 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -16,3 +16,4 @@ export * from './google'; export * from './github'; +export * from './okta'; diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts new file mode 100644 index 0000000000..7f2bd5d6bc --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts @@ -0,0 +1,111 @@ +/* + * 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 OktaAuth from './OktaAuth'; + +const theFuture = new Date(Date.now() + 3600000); +const thePast = new Date(Date.now() - 10); + +describe('OktaAuth', () => { + it('should get refreshed access token', async () => { + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, + }); + const oktaAuth = new OktaAuth({ getSession } as any); + + expect(await oktaAuth.getAccessToken()).toBe('access-token'); + expect(getSession).toBeCalledTimes(1); + }); + + it('should get refreshed id token', async () => { + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { idToken: 'id-token', expiresAt: theFuture }, + }); + const oktaAuth = new OktaAuth({ getSession } as any); + + expect(await oktaAuth.getIdToken()).toBe('id-token'); + expect(getSession).toBeCalledTimes(1); + }); + + it('should get optional id token', async () => { + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { idToken: 'id-token', expiresAt: theFuture }, + }); + const oktaAuth = new OktaAuth({ getSession } as any); + + expect(await oktaAuth.getIdToken({ optional: true })).toBe('id-token'); + expect(getSession).toBeCalledTimes(1); + }); + + it('should share popup closed errors', async () => { + const error = new Error('NOPE'); + error.name = 'RejectedError'; + const getSession = jest + .fn() + .mockResolvedValueOnce({ + providerInfo: { + accessToken: 'access-token', + expiresAt: theFuture, + scopes: new Set([`not-a-scope`]), + }, + }) + .mockRejectedValue(error); + const oktaAuth = new OktaAuth({ getSession } as any); + + // Make sure we have a session before we do the double request, so that we get past the !this.currentSession check + await expect(oktaAuth.getAccessToken()).resolves.toBe('access-token'); + + const promise1 = oktaAuth.getAccessToken('more'); + const promise2 = oktaAuth.getAccessToken('more'); + await expect(promise1).rejects.toBe(error); + await expect(promise2).rejects.toBe(error); + expect(getSession).toBeCalledTimes(3); + }); + + it('should wait for all session refreshes', async () => { + const initialSession = { + providerInfo: { + idToken: 'token1', + expiresAt: theFuture, + scopes: new Set(), + }, + }; + const getSession = jest + .fn() + .mockResolvedValueOnce(initialSession) + .mockResolvedValue({ + providerInfo: { + idToken: 'token2', + expiresAt: theFuture, + scopes: new Set(), + }, + }); + const oktaAuth = new OktaAuth({ getSession } as any); + + // Grab the expired session first + await expect(oktaAuth.getIdToken()).resolves.toBe('token1'); + expect(getSession).toBeCalledTimes(1); + + initialSession.providerInfo.expiresAt = thePast; + + const promise1 = oktaAuth.getIdToken(); + const promise2 = oktaAuth.getIdToken(); + const promise3 = oktaAuth.getIdToken(); + await expect(promise1).resolves.toBe('token2'); + await expect(promise2).resolves.toBe('token2'); + await expect(promise3).resolves.toBe('token2'); + expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client + }); +}); diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts new file mode 100644 index 0000000000..55820375f6 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -0,0 +1,168 @@ +/* + * 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 OktaIcon from '@material-ui/icons/AcUnit'; +import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; +import { OktaSession } from './types'; +import { + OAuthApi, + OpenIdConnectApi, + ProfileInfoApi, + ProfileInfo, + SessionStateApi, + SessionState, + BackstageIdentityApi, + AuthRequestOptions, + BackstageIdentity, +} from '../../../definitions/auth'; +import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; +import { Observable } from '../../../../types'; + +type CreateOptions = { + apiOrigin: string; + basePath: string; + + oauthRequestApi: OAuthRequestApi; + + environment?: string; + provider?: AuthProvider & { id: string }; +}; + +export type OktaAuthResponse = { + providerInfo: { + accessToken: string; + idToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +const DEFAULT_PROVIDER = { + id: 'okta', + title: 'Okta', + icon: OktaIcon, +}; + +class OktaAuth implements + OAuthApi, + OpenIdConnectApi, + ProfileInfoApi, + BackstageIdentityApi, + SessionStateApi +{ + static create({ + apiOrigin, + basePath, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + }: CreateOptions) { + const connector = new DefaultAuthConnector({ + apiOrigin, + basePath, + environment, + provider, + oauthRequestApi: oauthRequestApi, + sessionTransform(res: OktaAuthResponse): OktaSession { + return { + ...res, + providerInfo: { + idToken: res.providerInfo.idToken, + accessToken: res.providerInfo.accessToken, + scopes: OktaAuth.normalizeScopes(res.providerInfo.scope), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, + }; + }, + }); + + const sessionManager = new RefreshingAuthSessionManager({ + connector, + defaultScopes: new Set([ + 'openid', + 'email', + 'profile', + 'offline_access', + ]), + sessionScopes: session => session.scopes, + sessionShouldRefresh: session => { + const expiresInSec = + (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; + return expiresInSec < 60 * 5; + }, + }); + + return new OktaAuth(sessionManager); + } + + sessionState$(): Observable { + return this.sessionManager.sessionState$(); + } + + constructor(private readonly sessionManager: SessionManager) {} + + async getAccessToken( + scope?: string, + options?: AuthRequestOptions + ) { + const session = await this.sessionManager.getSession({ + ...options, + scopes: OktaAuth.normalizeScopes(scope), + }); + return session?.providerInfo.accessToken ?? ''; + } + + async getIdToken(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.providerInfo.idToken ?? ''; + } + + async logout() { + await this.sessionManager.removeSession(); + } + + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { + const session = await this.sessionManager.getSession(options); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.profile; + } + + static normalizeScopes(scope?: string): Set { + if (!scope) { + return new Set(); + } + + const scopeList = Array.isArray(scope) + ? scope + : scope.split(/[\s|,]/).filter(Boolean); + + return new Set(scopeList); + } +} + +export default OktaAuth; \ No newline at end of file diff --git a/packages/core-api/src/apis/implementations/auth/okta/index.ts b/packages/core-api/src/apis/implementations/auth/okta/index.ts new file mode 100644 index 0000000000..2bef0ce0db --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/okta/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; +export { default as OktaAuth } from './OktaAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/okta/types.ts b/packages/core-api/src/apis/implementations/auth/okta/types.ts new file mode 100644 index 0000000000..e3392ba1d6 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/okta/types.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 { ProfileInfo, BackstageIdentity } from '../../../definitions'; + +export type OktaSession = { + providerInfo: { + idToken: string; + accessToken: string; + scopes: Set; + expiresAt: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 36e8e1088b..668cb1354a 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -23,6 +23,7 @@ import { googleAuthApiRef, githubAuthApiRef, identityApiRef, + oktaAuthApiRef, useApi, } from '@backstage/core-api'; import { @@ -56,6 +57,11 @@ export function SidebarUserSettings() { apiRef={githubAuthApiRef} icon={Star} /> + { + const oktaAuthApi = useApi(oktaAuthApiRef); + const errorApi = useApi(errorApiRef); + + const handleLogin = async () => { + try { + const identity = await oktaAuthApi.getBackstageIdentity({ + instantPopup: true, + }); + + const profile = await oktaAuthApi.getProfile(); + + onResult({ + userId: identity!.id, + profile: profile!, + getIdToken: () => + oktaAuthApi.getBackstageIdentity().then(i => i!.idToken), + logout: async () => { + await oktaAuthApi.logout(); + }, + }); + } catch (error) { + errorApi.post(error); + } + }; + + return ( + + + Sign In + + } + > + Sign In using Okta + + + ); +}; + +const loader: ProviderLoader = async apis => { + const oktaAuthApi = apis.get(oktaAuthApiRef)!; + + const identity = await oktaAuthApi.getBackstageIdentity({ + optional: true, + }); + + if (!identity) { + return undefined; + } + + const profile = await oktaAuthApi.getProfile(); + + return { + userId: identity.id, + profile: profile!, + getIdToken: () => + oktaAuthApi.getBackstageIdentity().then(i => i!.idToken), + logout: async () => { + await oktaAuthApi.logout(); + }, + }; +}; + +export const oktaProvider: SignInProvider = { Component, loader }; \ No newline at end of file diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index 96dbc30a89..a195b2042a 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -18,6 +18,7 @@ import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; import { guestProvider } from './guestProvider'; import { googleProvider } from './googleProvider'; import { customProvider } from './customProvider'; +import { oktaProvider } from './oktaProvider'; import { SignInPageProps, SignInResult, @@ -30,12 +31,13 @@ import { SignInProvider } from './types'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; // Separate list here to avoid exporting internal types -export type SignInProviderId = 'guest' | 'google' | 'custom'; +export type SignInProviderId = 'guest' | 'google' | 'custom' | 'okta'; const signInProviders: { [id in SignInProviderId]: SignInProvider } = { guest: guestProvider, google: googleProvider, custom: customProvider, + okta: oktaProvider, }; export const useSignInProviders = ( diff --git a/yarn.lock b/yarn.lock index d1dbcf77ab..4a771bd6f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14111,7 +14111,16 @@ passport-google-oauth20@^2.0.0: dependencies: passport-oauth2 "1.x.x" -passport-oauth2@1.x.x: +passport-oauth1@1.x.x: + version "1.1.0" + resolved "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918" + integrity sha1-p96YiiEfnPRoc3cTDqdN8ycwyRg= + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + utils-merge "1.x.x" + +passport-oauth2@1.x.x, passport-oauth2@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== @@ -14122,6 +14131,23 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" +passport-oauth@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/passport-oauth/-/passport-oauth-1.0.0.tgz#90aff63387540f02089af28cdad39ea7f80d77df" + integrity sha1-kK/2M4dUDwIImvKM2tOep/gNd98= + dependencies: + passport-oauth1 "1.x.x" + passport-oauth2 "1.x.x" + +passport-okta-oauth@^0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/passport-okta-oauth/-/passport-okta-oauth-0.0.1.tgz#c8bcee02af3d56ca79d3cca776f2df7cf15a5748" + integrity sha1-yLzuAq89Vsp508yndvLffPFaV0g= + dependencies: + passport-oauth "1.0.0" + pkginfo "0.2.x" + uid2 "0.0.3" + passport-saml@^1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-1.3.3.tgz#cbea1a2b21ff32b3bc4bfd84dc39c3a370df9935" @@ -14370,6 +14396,11 @@ pkg-up@3.1.0, pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +pkginfo@0.2.x: + version "0.2.3" + resolved "https://registry.npmjs.org/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" + integrity sha1-cjnEKl72wwuPMoQ52bn/cQQkkPg= + please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -18365,7 +18396,7 @@ uid-number@0.0.6: resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= -uid2@0.0.x: +uid2@0.0.3, uid2@0.0.x: version "0.0.3" resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= From af07e2cb8b13616840dd42f081170c8b2f331773 Mon Sep 17 00:00:00 2001 From: Nicholas Pirrello Date: Thu, 25 Jun 2020 08:44:55 -0400 Subject: [PATCH 19/47] Add correct link for okta resouce scopes --- packages/core-api/src/apis/definitions/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index f82352cbc8..a944e85a7e 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -237,7 +237,7 @@ export const githubAuthApiRef = createApiRef< /** * Provides authentication towards Okta APIs. * - * See https://developer.okta.com/docs/reference/api/oidc/ + * See https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/ * for a full list of supported scopes. */ export const oktaAuthApiRef = createApiRef< From 180dbfded9d4723b30aee855fa89de1d803889c8 Mon Sep 17 00:00:00 2001 From: Nicholas Pirrello Date: Thu, 25 Jun 2020 18:20:13 -0400 Subject: [PATCH 20/47] Fix build and add okta oauth2 scope normalization --- .../auth/okta/OktaAuth.test.ts | 18 +++++++++++++++ .../implementations/auth/okta/OktaAuth.ts | 22 +++++++++++++++++-- packages/storybook/.storybook/apis.js | 11 ++++++++++ .../src/providers/okta/provider.ts | 4 ++-- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts index 7f2bd5d6bc..ab6c46c9b4 100644 --- a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts +++ b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.test.ts @@ -18,6 +18,8 @@ import OktaAuth from './OktaAuth'; const theFuture = new Date(Date.now() + 3600000); const thePast = new Date(Date.now() - 10); +const PREFIX = 'okta.'; + describe('OktaAuth', () => { it('should get refreshed access token', async () => { const getSession = jest.fn().mockResolvedValue({ @@ -108,4 +110,20 @@ describe('OktaAuth', () => { await expect(promise3).resolves.toBe('token2'); expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client }); + + it.each([ + ['openid', ['openid']], + ['profile email', ['profile', 'email']], + [`${PREFIX}groups.manage`, [`${PREFIX}groups.manage`]], + ['groups.read', [`${PREFIX}groups.read`]], + [`${PREFIX}groups.manage groups.read, openid`, [`${PREFIX}groups.manage`, `${PREFIX}groups.read`, 'openid']], + [`email\t ${PREFIX}groups.read`, ['email', `${PREFIX}groups.read`]], + + // Some incorrect scopes that we don't try to fix + [`${PREFIX}email`, [`${PREFIX}email`]], + [`${PREFIX}profile`, [`${PREFIX}profile`]], + [`${PREFIX}openid`, [`${PREFIX}openid`]], + ])(`should normalize scopes correctly - %p`, (scope, scopes) => { + expect(OktaAuth.normalizeScopes(scope)).toEqual(new Set(scopes)); + }); }); diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts index 55820375f6..5631648016 100644 --- a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -60,6 +60,12 @@ const DEFAULT_PROVIDER = { icon: OktaIcon, }; +const OKTA_OIDC_SCOPES: Set = new Set( + ['openid', 'profile', 'email', 'phone', 'address', 'groups', 'offline_access'] +) + +const OKTA_SCOPE_PREFIX: string = 'okta.' + class OktaAuth implements OAuthApi, OpenIdConnectApi, @@ -152,7 +158,7 @@ class OktaAuth implements return session?.profile; } - static normalizeScopes(scope?: string): Set { + static normalizeScopes(scope?: string | string[]): Set { if (!scope) { return new Set(); } @@ -161,7 +167,19 @@ class OktaAuth implements ? scope : scope.split(/[\s|,]/).filter(Boolean); - return new Set(scopeList); + const normalizedScopes = scopeList.map(scope => { + if (OKTA_OIDC_SCOPES.has(scope)) { + return scope; + } + + if (scope.startsWith(OKTA_SCOPE_PREFIX)) { + return scope; + } + + return `${OKTA_SCOPE_PREFIX}${scope}` + }); + + return new Set(normalizedScopes); } } diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 3b61ae7af8..0d0cf74e8f 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -6,11 +6,13 @@ import { OAuthRequestManager, googleAuthApiRef, githubAuthApiRef, + oktaAuthApiRef, AlertApiForwarder, ErrorApiForwarder, ErrorAlerter, GoogleAuth, GithubAuth, + OktaAuth, identityApiRef, } from '@backstage/core'; @@ -50,4 +52,13 @@ builder.add( }), ); +builder.add( + oktaAuthApiRef, + OktaAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), +); + export const apis = builder.build(); diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 7b3d5770d7..788af9c4bd 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -59,10 +59,10 @@ export class OktaAuthProvider implements OAuthProviderHandlers { * allowing us to avoid using express-session in order to integrate with Okta. */ private _store: StateStore = { - store(req: Request, cb: any) { + store({}, cb: any) { cb(null, null); }, - verify(req: Request, state: string, cb: any) { + verify({}, {}, cb: any) { cb(null, true); }, } From 9ba3d6a1248302653465188989c7f3614516e410 Mon Sep 17 00:00:00 2001 From: Nicholas Pirrello Date: Thu, 25 Jun 2020 18:28:51 -0400 Subject: [PATCH 21/47] Fix linting and tsc issues --- .../src/apis/implementations/auth/okta/OktaAuth.ts | 10 +++++----- plugins/auth-backend/src/providers/okta/provider.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts index 5631648016..f10ec4ebce 100644 --- a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -158,14 +158,14 @@ class OktaAuth implements return session?.profile; } - static normalizeScopes(scope?: string | string[]): Set { - if (!scope) { + static normalizeScopes(scopes?: string | string[]): Set { + if (!scopes) { return new Set(); } - const scopeList = Array.isArray(scope) - ? scope - : scope.split(/[\s|,]/).filter(Boolean); + const scopeList = Array.isArray(scopes) + ? scopes + : scopes.split(/[\s|,]/).filter(Boolean); const normalizedScopes = scopeList.map(scope => { if (OKTA_OIDC_SCOPES.has(scope)) { diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 788af9c4bd..2dd3b57137 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import express, { Request } from 'express'; +import express from 'express'; import { OAuthProvider } from '../../lib/OAuthProvider'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; From fccbd11e4207f52a832dabc085e88d917d3f01b6 Mon Sep 17 00:00:00 2001 From: Nicholas Pirrello Date: Thu, 25 Jun 2020 19:42:32 -0400 Subject: [PATCH 22/47] Add prompt: consent. Adjust unused types for StateStore --- plugins/auth-backend/src/providers/okta/provider.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 2dd3b57137..e73843bb0c 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -59,10 +59,10 @@ export class OktaAuthProvider implements OAuthProviderHandlers { * allowing us to avoid using express-session in order to integrate with Okta. */ private _store: StateStore = { - store({}, cb: any) { + store(_req: express.Request, cb: any) { cb(null, null); }, - verify({}, {}, cb: any) { + verify(_req: express.Request, _state: string, cb: any) { cb(null, true); }, } @@ -104,7 +104,12 @@ export class OktaAuthProvider implements OAuthProviderHandlers { req: express.Request, options: Record ): Promise { - return await executeRedirectStrategy(req, this._strategy, options); + const providerOptions = { + ...options, + accessType: 'offline', + prompt: 'consent', + }; + return await executeRedirectStrategy(req, this._strategy, providerOptions); } async handler( From 3be96fbba02c07ce1b980119be26e8fd75d2ec11 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Jun 2020 10:58:33 +0200 Subject: [PATCH 23/47] cli: implement postpack --- packages/cli/src/commands/pack.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/pack.ts b/packages/cli/src/commands/pack.ts index 51c7e36086..1f81b89767 100644 --- a/packages/cli/src/commands/pack.ts +++ b/packages/cli/src/commands/pack.ts @@ -19,10 +19,15 @@ import { paths } from '../lib/paths'; const SKIPPED_KEYS = ['access', 'registry', 'tag']; -export const pre = async () => { - const pkgPath = paths.resolveTarget('package.json'); +const PKG_PATH = 'package.json'; +const PKG_BACKUP_PATH = 'package.json-prepack'; - const pkg = await fs.readJson(pkgPath); +export const pre = async () => { + const pkgPath = paths.resolveTarget(PKG_PATH); + + const pkgContent = await fs.readFile(pkgPath, 'utf8'); + const pkg = JSON.parse(pkgContent); + await fs.writeFile(PKG_BACKUP_PATH, pkgContent); for (const key of Object.keys(pkg.publishConfig ?? {})) { if (!SKIPPED_KEYS.includes(key)) { @@ -33,5 +38,6 @@ export const pre = async () => { }; export const post = async () => { - // postpack is a noop for now, since it's not called anyway + // postpack isn't called by yarn right now, so it needs to be called manually + await fs.move(PKG_BACKUP_PATH, PKG_PATH, { overwrite: true }); }; From ad38aea485f62bbda262dee3aea15499b1be22a6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Jun 2020 11:39:28 +0200 Subject: [PATCH 24/47] cli: rename packager to builder --- packages/cli/src/commands/backend/build.ts | 2 +- packages/cli/src/commands/build.ts | 2 +- packages/cli/src/commands/plugin/build.ts | 2 +- packages/cli/src/lib/{packager => builder}/config.ts | 0 packages/cli/src/lib/{packager => builder}/index.ts | 0 packages/cli/src/lib/{packager => builder}/packager.ts | 0 packages/cli/src/lib/{packager => builder}/types.ts | 0 7 files changed, 3 insertions(+), 3 deletions(-) rename packages/cli/src/lib/{packager => builder}/config.ts (100%) rename packages/cli/src/lib/{packager => builder}/index.ts (100%) rename packages/cli/src/lib/{packager => builder}/packager.ts (100%) rename packages/cli/src/lib/{packager => builder}/types.ts (100%) diff --git a/packages/cli/src/commands/backend/build.ts b/packages/cli/src/commands/backend/build.ts index a9cdc51fa9..ceca5b0286 100644 --- a/packages/cli/src/commands/backend/build.ts +++ b/packages/cli/src/commands/backend/build.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { buildPackage, Output } from '../../lib/packager'; +import { buildPackage, Output } from '../../lib/builder'; export default async () => { await buildPackage({ diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 19e4b17352..bd5bbc5e9f 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { buildPackage, Output } from '../lib/packager'; +import { buildPackage, Output } from '../lib/builder'; import { Command } from 'commander'; export default async (cmd: Command) => { diff --git a/packages/cli/src/commands/plugin/build.ts b/packages/cli/src/commands/plugin/build.ts index 7e4e5cd36a..d62ffbeebd 100644 --- a/packages/cli/src/commands/plugin/build.ts +++ b/packages/cli/src/commands/plugin/build.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { buildPackage, Output } from '../../lib/packager'; +import { buildPackage, Output } from '../../lib/builder'; export default async () => { await buildPackage({ diff --git a/packages/cli/src/lib/packager/config.ts b/packages/cli/src/lib/builder/config.ts similarity index 100% rename from packages/cli/src/lib/packager/config.ts rename to packages/cli/src/lib/builder/config.ts diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/builder/index.ts similarity index 100% rename from packages/cli/src/lib/packager/index.ts rename to packages/cli/src/lib/builder/index.ts diff --git a/packages/cli/src/lib/packager/packager.ts b/packages/cli/src/lib/builder/packager.ts similarity index 100% rename from packages/cli/src/lib/packager/packager.ts rename to packages/cli/src/lib/builder/packager.ts diff --git a/packages/cli/src/lib/packager/types.ts b/packages/cli/src/lib/builder/types.ts similarity index 100% rename from packages/cli/src/lib/packager/types.ts rename to packages/cli/src/lib/builder/types.ts From af48afcb09431d33135be1e8bc2bac582d837246 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Jun 2020 17:15:23 +0200 Subject: [PATCH 25/47] cli: add packager lib for packaging packages into a dist workspace Co-authored-by: Ivan Shmidt Co-authored-by: Raghunandan --- packages/cli/src/lib/packager/index.ts | 142 +++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 packages/cli/src/lib/packager/index.ts diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts new file mode 100644 index 0000000000..c702fd3df4 --- /dev/null +++ b/packages/cli/src/lib/packager/index.ts @@ -0,0 +1,142 @@ +/* + * 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 fs from 'fs-extra'; +import { resolve as resolvePath, relative as relativePath } from 'path'; +import { paths } from '../paths'; +import { run } from '../run'; +import tar from 'tar'; +import { tmpdir } from 'os'; + +type LernaPackage = { + name: string; + private: boolean; + location: string; +}; + +type FileEntry = + | string + | { + src: string; + dest: string; + }; + +type Options = { + /** + * Target directory for the dist workspace, defaults to a temporary directory + */ + targetDir?: string; + + /** + * Files to copy into the target workspace. + * + * Defaults to ['yarn.lock', 'package.json']. + */ + files?: FileEntry[]; +}; + +/** + * Uses `yarn pack` to package local packages and unpacks them into a dist workspace. + * The target workspace will end up containing dist version of each package and + * will be suitable for packaging e.g. into a docker image. + * + * This creates a structure that is functionally similar to if the packages where + * installed from NPM, but uses yarn workspaces to link to them at runtime. + */ +export async function createDistWorkspace( + packageNames: string[], + options: Options, +) { + const targetDir = + options.targetDir ?? + (await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace'))); + + const targets = await findTargetPackages(packageNames); + + await moveToDistWorkspace(targetDir, targets); + + const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json']; + + for (const file of files) { + const src = typeof file === 'string' ? file : file.src; + const dest = typeof file === 'string' ? file : file.dest; + await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest)); + } +} + +async function moveToDistWorkspace( + workspaceDir: string, + localPackages: LernaPackage[], +): Promise { + await Promise.all( + localPackages.map(async (target, index) => { + console.log(`Repacking ${target.name} into dist workspace`); + const archive = `temp-package-${index}.tgz`; + const archivePath = resolvePath(workspaceDir, archive); + + await run('yarn', ['pack', '--filename', archivePath], { + cwd: target.location, + }); + // TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed + await run('yarn', ['postpack'], { cwd: target.location }); + + const outputDir = relativePath(paths.targetRoot, target.location); + const absoluteOutputPath = resolvePath(workspaceDir, outputDir); + await fs.ensureDir(absoluteOutputPath); + + await tar.extract({ + file: archivePath, + cwd: absoluteOutputPath, + strip: 1, + }); + await fs.remove(archivePath); + }), + ); +} + +async function findTargetPackages(pkgNames: string[]): Promise { + const LernaProject = require('@lerna/project'); + const PackageGraph = require('@lerna/package-graph'); + + const project = new LernaProject(paths.targetDir); + const packages = await project.getPackages(); + const graph = new PackageGraph(packages); + + const targets = new Map(); + const searchNames = pkgNames.slice(); + + while (searchNames.length) { + const name = searchNames.pop()!; + + if (targets.has(name)) { + continue; + } + + const node = graph.get(name); + if (!node) { + throw new Error(`Package '${name}' not found`); + } + + const pkgDeps = Object.keys(node.pkg.dependencies); + const localDeps: string[] = Array.from(node.localDependencies.keys()); + const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep)); + + searchNames.push(...filteredDeps); + targets.set(name, node.pkg); + } + + return Array.from(targets.values()); +} From be3ae0221542473018bed79a01da7e8eba7bf472 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Jun 2020 18:03:21 +0200 Subject: [PATCH 26/47] cli: added backend:build-image command Co-authored-by: Ivan Shmidt --- .../cli/src/commands/backend/buildImage.ts | 39 +++++++++++++++++++ packages/cli/src/index.ts | 9 +++++ packages/cli/src/lib/packager/index.ts | 1 + 3 files changed, 49 insertions(+) create mode 100644 packages/cli/src/commands/backend/buildImage.ts diff --git a/packages/cli/src/commands/backend/buildImage.ts b/packages/cli/src/commands/backend/buildImage.ts new file mode 100644 index 0000000000..a847f1ef54 --- /dev/null +++ b/packages/cli/src/commands/backend/buildImage.ts @@ -0,0 +1,39 @@ +/* + * 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 fs from 'fs-extra'; +import { createDistWorkspace } from '../../lib/packager'; +import { paths } from '../../lib/paths'; +import { run } from '../../lib/run'; + +export default async (imageTag: string) => { + const tempDistWorkspace = await createDistWorkspace(['example-backend'], { + files: [ + 'package.json', + 'yarn.lock', + 'app-config.yaml', + { src: paths.resolveTarget('Dockerfile'), dest: 'Dockerfile' }, + ], + }); + + console.log(`Dist workspace ready at ${tempDistWorkspace}`); + + await run('docker', ['build', '.', '-t', imageTag], { + cwd: tempDistWorkspace, + }); + + await fs.remove(tempDistWorkspace); +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e67a52c795..bed32bcf8b 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -46,6 +46,15 @@ const main = (argv: string[]) => { .description('Build a backend plugin') .action(lazyAction(() => import('./commands/backend/build'), 'default')); + program + .command('backend:build-image ') + .description( + 'Builds a docker image from the package, with all local deps included', + ) + .action( + lazyAction(() => import('./commands/backend/buildImage'), 'default'), + ); + program .command('backend:dev') .description('Start local development server with HMR for the backend') diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index c702fd3df4..2f5233f93f 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -75,6 +75,7 @@ export async function createDistWorkspace( const dest = typeof file === 'string' ? file : file.dest; await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest)); } + return targetDir; } async function moveToDistWorkspace( From 66ef3e9821c63b1f682c98264f62970c68b648b6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Jun 2020 18:05:11 +0200 Subject: [PATCH 27/47] backend: update packaging and add build-image command + Dockerfile Co-authored-by: Ivan Shmidt --- packages/backend/Dockerfile | 9 +++++++++ packages/backend/package.json | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 packages/backend/Dockerfile diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile new file mode 100644 index 0000000000..3e8ba36cec --- /dev/null +++ b/packages/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM node:12 + +WORKDIR /usr/src/app + +COPY . . + +RUN yarn install --frozen-lockfile --production + +CMD ["node", "packages/backend"] diff --git a/packages/backend/package.json b/packages/backend/package.json index 3be1f5ee3e..53b1f431c0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,19 +1,27 @@ { "name": "example-backend", "version": "0.1.1-alpha.12", - "main": "dist/index.cjs.js", + "main": "src/index.ts", "types": "src/index.ts", "private": true, "license": "Apache-2.0", "engines": { "node": ">=12" }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli backend:build", + "build-image": "backstage-cli backend:build-image example-backend", "start": "backstage-cli backend:dev", "lint": "backstage-cli lint", "test": "backstage-cli test", "clean": "backstage-cli clean", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", "migrate:create": "knex migrate:make -x ts" }, "dependencies": { From e562274874e6d3173fb9ad4049f9661f2a615eed Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Jun 2020 18:05:45 +0200 Subject: [PATCH 28/47] plugins/auth-backend: include migrations in published package Co-authored-by: Ivan Shmidt --- plugins/auth-backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index d137357d60..a336441dc2 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -59,6 +59,7 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist" + "dist", + "migrations" ] } From 4c43b3e2ffa83c317c1e80bc7ad50099a0baced0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Jun 2020 19:56:18 +0200 Subject: [PATCH 29/47] packages/backend,cli: revert publish config and remove postpack script requirement from image-build --- packages/backend/package.json | 9 +-------- packages/cli/src/lib/packager/index.ts | 5 ++++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 53b1f431c0..64ca2c3113 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,18 +1,13 @@ { "name": "example-backend", "version": "0.1.1-alpha.12", - "main": "src/index.ts", + "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, "license": "Apache-2.0", "engines": { "node": ">=12" }, - "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" - }, "scripts": { "build": "backstage-cli backend:build", "build-image": "backstage-cli backend:build-image example-backend", @@ -20,8 +15,6 @@ "lint": "backstage-cli lint", "test": "backstage-cli test", "clean": "backstage-cli clean", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", "migrate:create": "knex migrate:make -x ts" }, "dependencies": { diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index 2f5233f93f..5e309ab953 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -25,6 +25,7 @@ type LernaPackage = { name: string; private: boolean; location: string; + scripts: Record; }; type FileEntry = @@ -92,7 +93,9 @@ async function moveToDistWorkspace( cwd: target.location, }); // TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed - await run('yarn', ['postpack'], { cwd: target.location }); + if (target.scripts.postpack) { + await run('yarn', ['postpack'], { cwd: target.location }); + } const outputDir = relativePath(paths.targetRoot, target.location); const absoluteOutputPath = resolvePath(workspaceDir, outputDir); From 71c65d96103097907973f756dd509b8bd56d34fd Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 11:39:26 +0200 Subject: [PATCH 30/47] feat(core): controlled stepper --- .../components/SimpleStepper/SimpleStepper.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 => { From 3f3409ac4c8a07403e49d8468799e8a2e9ed947e Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 11:42:17 +0200 Subject: [PATCH 31/47] feat(scaffolder): mocked fe flow --- package.json | 2 +- packages/app/src/apis.ts | 16 +- .../react-ssr-template/template.yaml | 5 + plugins/scaffolder/package.json | 2 + plugins/scaffolder/src/api.ts | 52 ++++++ .../src/components/CreatePage/CreatePage.tsx | 111 ++++++++++++ .../src/components/CreatePage/index.ts | 1 + .../components/JobStatusModal/JobStage.tsx | 149 ++++++++++++++++ .../JobStatusModal/JobStatusModal.tsx | 41 +++++ .../src/components/JobStatusModal/index.ts | 1 + .../src/components/JobStatusModal/jobMocks.ts | 78 +++++++++ .../src/components/JobStatusModal/types.ts | 23 +++ .../{ => ScaffolderPage}/TemplateCard.tsx | 18 +- .../src/components/ScaffolderPage/index.tsx | 7 +- plugins/scaffolder/src/index.ts | 3 +- plugins/scaffolder/src/plugin.ts | 13 +- plugins/scaffolder/src/routes.ts | 12 ++ yarn.lock | 162 +++++++++++++++++- 18 files changed, 668 insertions(+), 28 deletions(-) create mode 100644 plugins/scaffolder/src/api.ts create mode 100644 plugins/scaffolder/src/components/CreatePage/CreatePage.tsx create mode 100644 plugins/scaffolder/src/components/CreatePage/index.ts create mode 100644 plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx create mode 100644 plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx create mode 100644 plugins/scaffolder/src/components/JobStatusModal/index.ts create mode 100644 plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts create mode 100644 plugins/scaffolder/src/components/JobStatusModal/types.ts rename plugins/scaffolder/src/components/{ => ScaffolderPage}/TemplateCard.tsx (78%) create mode 100644 plugins/scaffolder/src/routes.ts 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 d2b2706464..1565dc294e 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -51,6 +51,10 @@ import { graphQlBrowseApiRef, GraphQLEndpoints, } from '@backstage/plugin-graphiql'; +import { + scaffolderApiRef, + ScaffolderApi, +} from '@backstage/plugin-scaffolder/src/api'; export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console @@ -113,8 +117,16 @@ export const apis = (config: ConfigApi) => { builder.add( catalogApiRef, new CatalogClient({ - apiOrigin: 'http://localhost:3000', - basePath: '/catalog/api', + apiOrigin: 'http://localhost:7000', + basePath: '/catalog', + }), + ); + + builder.add( + scaffolderApiRef, + new ScaffolderApi({ + apiOrigin: 'http://localhost:7000', + basePath: '/scaffolder/v1', }), ); 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..e01eb4cb45 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,8 @@ spec: processor: cookiecutter type: website path: '.' + parameters: + component_name: + title: Component name + type: string + description: Name of the component diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 6bd312921e..91b03d4184 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -28,6 +28,8 @@ "@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", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-alpha.5", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts new file mode 100644 index 0000000000..f87285a80c --- /dev/null +++ b/plugins/scaffolder/src/api.ts @@ -0,0 +1,52 @@ +/* + * 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; + } + + async scaffold( + template: TemplateEntityV1alpha1, + values: Record, + ) { + const url = `${this.apiOrigin}${this.basePath}/jobs`; + const jobId = await fetch(url, { + method: 'POST', + body: JSON.stringify({ template, values }), + }).then(x => x.json()); + + return jobId; + } +} diff --git a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx new file mode 100644 index 0000000000..f67a6dbd8e --- /dev/null +++ b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import useStaleWhileRevalidate from 'swr'; +import { useParams } from 'react-router-dom'; +import { LinearProgress, Button } from '@material-ui/core'; +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { + useApi, + SimpleStepper, + SimpleStepperStep, + Page, + Content, + ContentHeader, + Header, + Lifecycle, +} from '@backstage/core'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { withTheme, IChangeEvent } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; +import { JobStatusModal } from '../JobStatusModal'; +import { scaffolderApiRef } from '../../api'; + +const Form = withTheme(MuiTheme); + +export const CreatePage = () => { + const catalogApi = useApi(catalogApiRef); + const scaffolderApi = useApi(scaffolderApiRef); + const { templateName } = useParams(); + const { + data: [template] = [] as TemplateEntityV1alpha1[], + isValidating, + } = useStaleWhileRevalidate( + `templates/${templateName}`, + async () => + (catalogApi.getEntities({ + kind: 'Template', + 'metadata.name': templateName, + }) as any) as Promise, + ); + const [formState, setFormState] = useState({}); + + const handleChange = (e: IChangeEvent) => + setFormState({ ...formState, ...e.formData }); + + const [jobId, setJobId] = useState(null); + const handleClose = () => setJobId(null); + if (!template && isValidating) return ; + if (!template || !template?.spec?.parameters) return null; + + const handleCreate = async () => { + const job = await scaffolderApi.scaffold(template, formState); + setJobId(job); + }; + + return ( + +
+ Create a new component + + } + subtitle="Create new software components using standard templates" + /> + + + {jobId && } + { + if (nextStep === 2) { + handleCreate(); + } + }} + > + +
+
+
diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index 2f9774757b..1570006b44 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -35,11 +35,11 @@ import { LinearProgress, } from '@material-ui/core'; import { Link as RouterLink } from 'react-router-dom'; -import TemplateCard from '../TemplateCard'; +import TemplateCard from './TemplateCard'; import useStaleWhileRevalidate from 'swr'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -const ScaffolderPage: React.FC<{}> = () => { +export const ScaffolderPage: React.FC<{}> = () => { const catalogApi = useApi(catalogApiRef); const errorApi = useApi(errorApiRef); @@ -98,6 +98,7 @@ const ScaffolderPage: React.FC<{}> = () => { = () => { ); }; - -export default ScaffolderPage; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 5b20cb0158..3fd7d38c57 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { plugin, rootRoute } from './plugin'; +export { plugin } from './plugin'; +export { rootRoute, createTemplateRoute } from './routes'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index b2cb305e57..d0d38d5812 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 { CreatePage } from './components/CreatePage'; +import { rootRoute, createTemplateRoute } from './routes'; export const plugin = createPlugin({ id: 'scaffolder', register({ router }) { router.addRoute(rootRoute, ScaffolderPage); + router.addRoute(createTemplateRoute, CreatePage); }, }); diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts new file mode 100644 index 0000000000..f42f9540f9 --- /dev/null +++ b/plugins/scaffolder/src/routes.ts @@ -0,0 +1,12 @@ +import { createRouteRef } from '@backstage/core'; + +export const rootRoute = createRouteRef({ + icon: () => null, + path: '/create', + title: 'Create new entity', +}); +export const createTemplateRoute = createRouteRef({ + icon: () => null, + path: '/create/:templateName', + title: 'Entity creation', +}); diff --git a/yarn.lock b/yarn.lock index 4a771bd6f6..1601bef739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -876,6 +876,14 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" +"@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" @@ -2454,6 +2462,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" @@ -2561,10 +2591,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" @@ -3653,6 +3683,11 @@ 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": + 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== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -4519,7 +4554,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.0, ajv@^6.5.5: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, 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== @@ -4810,7 +4845,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= @@ -6380,6 +6415,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" @@ -6629,7 +6683,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.6.5: +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== @@ -6639,6 +6693,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" @@ -11644,6 +11703,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" @@ -11718,6 +11793,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" @@ -13087,6 +13167,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" @@ -15008,6 +15093,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" @@ -15218,6 +15310,13 @@ raf-schd@^4.0.0: 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" @@ -15296,6 +15395,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@11.0.3: version "11.0.3" resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#5678bb3e725d8b56cb7cf57f56e952105fc4f2af" @@ -16799,6 +16910,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" @@ -18800,6 +18918,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= + value-equal@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" From f937602641688ce3e2bfce4bf9e2df77553f3170 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 14:02:51 +0200 Subject: [PATCH 32/47] feat(scaffolder): do real fetch --- plugins/scaffolder/src/api.ts | 9 ++- .../components/JobStatusModal/JobStage.tsx | 27 ++++--- .../JobStatusModal/JobStatusModal.tsx | 7 +- .../src/components/JobStatusModal/jobMocks.ts | 78 ------------------- .../JobStatusModal/useJobPolling.ts | 41 ++++++++++ 5 files changed, 66 insertions(+), 96 deletions(-) delete mode 100644 plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts create mode 100644 plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index f87285a80c..04588d7177 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -42,11 +42,18 @@ export class ScaffolderApi { values: Record, ) { const url = `${this.apiOrigin}${this.basePath}/jobs`; - const jobId = await fetch(url, { + const { id: jobId } = await fetch(url, { method: 'POST', body: JSON.stringify({ template, values }), }).then(x => x.json()); return jobId; } + + 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/JobStatusModal/JobStage.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx index f1dec09f88..ef799d13b4 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx @@ -87,16 +87,10 @@ type Props = { className?: string; log: string[]; startedAt: string; - finishedAt?: string; + endedAt?: string; status?: Job['status']; }; -export const JobStage = ({ - finishedAt, - startedAt, - name, - log, - status, -}: Props) => { +export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { const classes = useStyles(); const [expanded, setExpanded] = useState(false); @@ -104,18 +98,23 @@ export const JobStage = ({ if (status === 'FAILED') setExpanded(true); }, [status === 'FAILED', setExpanded]); - const timeElapsed = moment - .duration(moment(finishedAt ?? moment()).diff(moment(startedAt))) - .humanize(); + const timeElapsed = + status !== 'PENDING' + ? moment + .duration(moment(endedAt ?? moment()).diff(moment(startedAt))) + .humanize() + : null; return ( - {name} ({timeElapsed}) + {name} {timeElapsed && `(${timeElapsed})`} diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx index f946df1bf3..c589e01127 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -6,7 +6,7 @@ import { DialogContent, } from '@material-ui/core'; import { JobStage } from './JobStage'; -import { useJob } from './jobMocks'; +import { useJobPolling } from './useJobPolling'; type Props = { onClose: () => void; @@ -14,7 +14,8 @@ type Props = { }; export const JobStatusModal = ({ onClose, jobId }: Props) => { - const job = useJob(jobId); + console.log({ jobId }); + const job = useJobPolling(jobId); return ( @@ -30,7 +31,7 @@ export const JobStatusModal = ({ onClose, jobId }: Props) => { name={step.name} key={step.name} startedAt={step.startedAt} - finishedAt={step.finishedAt} + endedAt={step.endedAt} status={step.status} /> )) diff --git a/plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts b/plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts deleted file mode 100644 index 08a3d3b720..0000000000 --- a/plugins/scaffolder/src/components/JobStatusModal/jobMocks.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { useMemo, useState, useEffect } from 'react'; -import { Job } from './types'; - -function* emulatePoll() { - const now = () => new Date().toString(); - const job: Job = { - id: '132536-42362-4253532', - metadata: { entity: {}, values: {} }, - status: 'STARTED', - stages: [ - { - name: 'created', - startedAt: now(), - log: [ - 'Job id #rw-tstywe-tdsy was successfully created and placed in the queue', - ], - status: 'STARTED', - }, - ], - }; - let newTime = now(); - job.stages[0].finishedAt = newTime; - job.stages.push({ - startedAt: newTime, - name: 'preparing', - log: ['preparing blahblah', 'some other stuuff'], - status: 'COMPLETE', - }); - yield job; - - newTime = now(); - job.stages[1].finishedAt = newTime; - job.stages.push({ - startedAt: newTime, - name: 'templating', - log: ['templating blahblah', 'some other stuuff'], - status: 'COMPLETE', - }); - yield job; - - newTime = now(); - job.stages[2].finishedAt = newTime; - job.stages.push({ - startedAt: newTime, - name: 'pushing', - log: ['pushing blahblah', 'some other stuuff'], - status: 'STARTED', - }); - yield job; - yield job; - job.stages[3].status = 'FAILED'; - job.stages[3].log.push('ERROR OCCURED'); - - while (true) yield job; -} - -export const useJob = (jobId: string | null) => { - const apiMock = useMemo(() => emulatePoll(), [jobId]); - const [job, setJob] = useState(undefined); - useEffect(() => { - if (!jobId) return; - const nextJobState = apiMock.next().value as Job; - setJob({ ...nextJobState }); - const intervalId = setInterval(() => { - const nextJobState = apiMock.next().value as Job; - - if (nextJobState?.status === 'FAILED') { - clearInterval(intervalId); - } - - setJob({ ...nextJobState }); - }, 3000); - return () => { - clearInterval(intervalId); - }; - }, [jobId, setJob]); - return job; -}; diff --git a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts new file mode 100644 index 0000000000..89b7228a9c --- /dev/null +++ b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'react'; +import { Job } from './types'; +import { useApi } from '@backstage/core'; +import { scaffolderApiRef } from '../../api'; + +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) => { + const scaffolderApi = useApi(scaffolderApiRef); + + const [job, setJob] = useState(undefined); + useEffect(() => { + if (!jobId) return; + const stopPolling = poll(async () => { + const nextJobState = await scaffolderApi.getJob(jobId); + if ( + nextJobState.status === 'FAILED' || + nextJobState.status === 'COMPLETED' + ) { + stopPolling(); + } + setJob(nextJobState); + }, 500); + return () => { + stopPolling(); + }; + }, [jobId, setJob]); + return job; +}; From 26d4bda16fa38a0dffa970a7881ea5a43df7f0f4 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 14:22:35 +0200 Subject: [PATCH 33/47] fix(scaffolder): integrate with backend --- .../stages/templater/cookiecutter.ts | 2 +- .../scaffolder-backend/src/service/router.ts | 34 ++++++------------- plugins/scaffolder/src/api.ts | 3 ++ 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index 5e875b6838..528280e7fd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -50,7 +50,7 @@ export class CookieCutter implements TemplaterBase { const resultDir = await fs.promises.mkdtemp(`${options.directory}-result`); await runDockerContainer({ - imageName: 'spotify/backstage-cookiecutter', + imageName: 'backstage/cookiecutter', args: [ 'cookiecutter', '--no-input', diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index da7dce40f9..e9f644bcc7 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -20,7 +20,7 @@ import express from 'express'; import { PreparerBuilder, TemplaterBase, JobProcessor } from '../scaffolder'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import Docker from 'dockerode'; -import {} from '@backstage/backend-common'; +import { InputError } from '@backstage/backend-common'; import { StageContext } from '../scaffolder/jobs/types'; import { Octokit } from '@octokit/rest'; import { GithubStorer } from '../scaffolder/stages/store/github'; @@ -76,36 +76,22 @@ export async function createRouter( error: job.error, }); }) - .post('/v1/jobs', async (_, res) => { + .post('/v1/jobs', async (req, res) => { // TODO(blam): Create a unique job here and return the ID so that // The end user can poll for updates on the current job // TODO(blam): Take this entity from the post body sent from the frontend - const mockEntity: TemplateEntityV1alpha1 = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'file:/Users/blam/dev/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml', - }, - name: 'graphql-starter', - title: 'GraphQL Service', - description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', - uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', - etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', + console.log(req.body); + const template: TemplateEntityV1alpha1 = req.body.template; - generation: 1, - }, - spec: { - type: 'cookiecutter', - path: '.', - }, - }; + if (!template) { + throw new InputError( + 'You should specify the template to scaffold from', + ); + } const job = jobProcessor.create({ - entity: mockEntity, + entity: template, values: { component_id: `blob${Date.now()}`, org: 'hojden', diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 04588d7177..e84adb45f8 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -44,6 +44,9 @@ export class ScaffolderApi { const url = `${this.apiOrigin}${this.basePath}/jobs`; const { id: jobId } = await fetch(url, { method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ template, values }), }).then(x => x.json()); From 97f3d6b0563c1562eb247de6242dc23efdad825a Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 14:55:00 +0200 Subject: [PATCH 34/47] fix(scaffolder): make integration work --- .../react-ssr-template/template.yaml | 19 ++++-- .../src/scaffolder/jobs/processor.ts | 1 + .../src/components/CreatePage/CreatePage.tsx | 25 ++++++-- yarn.lock | 63 ++++++++----------- 4 files changed, 62 insertions(+), 46 deletions(-) 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 e01eb4cb45..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,8 +11,17 @@ spec: processor: cookiecutter type: website path: '.' - parameters: - component_name: - title: Component name - type: string - description: Name of the component + 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.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index 1c76ae252d..c33ff13e00 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/src/components/CreatePage/CreatePage.tsx b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx index f67a6dbd8e..23b7232cf4 100644 --- a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx +++ b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import React, { useState } from 'react'; import useStaleWhileRevalidate from 'swr'; import { useParams } from 'react-router-dom'; @@ -43,8 +58,9 @@ export const CreatePage = () => { const [jobId, setJobId] = useState(null); const handleClose = () => setJobId(null); + if (!template && isValidating) return ; - if (!template || !template?.spec?.parameters) return null; + if (!template || !template?.spec?.schema) return null; const handleCreate = async () => { const job = await scaffolderApi.scaffold(template, formState); @@ -63,10 +79,9 @@ export const CreatePage = () => { subtitle="Create new software components using standard templates" /> - + {jobId && } + {/* { if (nextStep === 2) { @@ -80,7 +95,7 @@ export const CreatePage = () => { onChange={handleChange} schema={{ $schema: 'http://json-schema.org/draft-07/schema#', - properties: template?.spec?.parameters, + ...template?.spec?.schema, }} > + + )} ); }; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx new file mode 100644 index 0000000000..7e8f470fd9 --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { withTheme, FormProps, IChangeEvent } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; +import { + Stepper, + Step, + StepLabel, + StepContent, + Button, + Paper, + Typography, + Box, +} from '@material-ui/core'; +import { Content, StructuredMetadataTable } from '@backstage/core'; + +const Form = withTheme(MuiTheme); +type Step = { + schema: FormProps['schema']; + label: string; +}; +type Props = { + 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 }) => ( + + {label} + +
{ + if (e.errors.length === 0) handleNext(); + }} + > +
+
+ + +
+
+
+
+
+ ))} +
+ {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..6cb93273a6 --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/index.ts @@ -0,0 +1 @@ +export { MultistepJsonForm } from './MultistepJsonForm'; From cf51790a77e9f4d42f362d52c1e2df467e1bec93 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 29 Jun 2020 23:02:56 +0200 Subject: [PATCH 36/47] fix: hooks order --- .../scaffolder/src/components/CreatePage/CreatePage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx index 6c69ce975b..458389202d 100644 --- a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx +++ b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx @@ -59,9 +59,6 @@ export const CreatePage = () => { const [jobId, setJobId] = useState(null); const handleClose = () => setJobId(null); - if (!template && isValidating) return ; - if (!template || !template?.spec?.schema) return null; - const handleCreate = async () => { const job = await scaffolderApi.scaffold(template, formState); setJobId(job); @@ -71,7 +68,6 @@ export const CreatePage = () => { null, ); const handleCreateComplete = async (job: Job) => { - console.log('DEBUG:', { job }); const { entities: [createdEntity], } = await catalogApi.addLocation( @@ -83,6 +79,10 @@ export const CreatePage = () => { ); setEntity(createdEntity); }; + + if (!template && isValidating) return ; + if (!template || !template?.spec?.schema) return null; + return (
Date: Mon, 29 Jun 2020 23:14:18 +0200 Subject: [PATCH 37/47] fix(scaffolder): adjust schema --- .../scaffolder/src/components/CreatePage/CreatePage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx index 458389202d..4fb924e1f3 100644 --- a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx +++ b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx @@ -118,17 +118,17 @@ export const CreatePage = () => { label: 'Choose owner and repo', schema: { $schema: 'http://json-schema.org/draft-07/schema#', - required: ['repo', 'owner'], + required: ['storePath', 'owner'], properties: { owner: { type: 'string', title: 'Owner', description: 'Who is going to own this component', }, - repo: { + storePath: { type: 'string', - title: 'GitHub repository', - description: 'Repo where to upload created component', + title: 'Store path', + description: 'GitHub store path in org/repo format', }, }, }, From e4dd9bd5b23ddaea40de369069a0f7042d7fe9d7 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 30 Jun 2020 15:08:02 +0200 Subject: [PATCH 38/47] fix: correct docker image --- .../src/scaffolder/stages/templater/cookiecutter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index 27f0f9c11b..3e94d67c6e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -50,7 +50,7 @@ export class CookieCutter implements TemplaterBase { const resultDir = await fs.promises.mkdtemp(`${options.directory}-result`); await runDockerContainer({ - imageName: 'backstage/cookiecutter', + imageName: 'spotify/backstage-cookiecutter', args: [ 'cookiecutter', '--no-input', From 34399824a6c301c542d567a29afabcf9264c6ba9 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 1 Jul 2020 18:51:33 +0200 Subject: [PATCH 39/47] fix(scaffolder): schema adjusted --- packages/catalog-model/package.json | 1 + .../src/kinds/TemplateEntityV1alpha1.test.ts | 16 ++++++++++++++++ .../src/kinds/TemplateEntityV1alpha1.ts | 4 +++- packages/catalog-model/src/types.ts | 4 ++++ plugins/scaffolder/src/api.ts | 16 +++++++++++++--- yarn.lock | 5 +++++ 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 889d9d2e02..64ac3b3221 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -22,6 +22,7 @@ "dependencies": { "@backstage/config": "^0.1.1-alpha.12", "@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/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/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index e84adb45f8..585a49ad78 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -37,20 +37,30 @@ export class ScaffolderApi { 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 { id: jobId } = await fetch(url, { + const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ template, values }), - }).then(x => x.json()); + }); - return jobId; + if (response.status !== 201) { + throw new Error(await response.text()); + } + + const { id } = await response.json(); + return id; } async getJob(jobId: string) { diff --git a/yarn.lock b/yarn.lock index f3e2129cc9..8b28254a52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11721,6 +11721,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" From 227a4af56192abd41c46cd68f5140246a9588bab Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 1 Jul 2020 19:15:06 +0200 Subject: [PATCH 40/47] fix(scaffolder): typefixes --- packages/catalog-model/src/index.ts | 2 +- .../src/scaffolder/jobs/processor.test.ts | 16 ++++ .../scaffolder/stages/prepare/github.test.ts | 16 ++++ .../scaffolder/stages/prepare/helpers.test.ts | 80 +++++++++++++++++++ .../stages/prepare/preparers.test.ts | 32 ++++++++ .../scaffolder-backend/src/service/router.ts | 28 +++---- .../src/components/CreatePage/CreatePage.tsx | 26 ++++-- .../src/components/JobStatusModal/types.ts | 27 +++++-- .../JobStatusModal/useJobPolling.ts | 30 +++++-- .../MultistepJsonForm/MultistepJsonForm.tsx | 22 ++++- 10 files changed, 240 insertions(+), 39 deletions(-) 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/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/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 86377be6d0..e1f7bdc4a8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -50,6 +50,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 1511a2b726..b370c23fa3 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -14,22 +14,22 @@ * limitations under the License. */ -import { Logger } from 'winston'; -import Router from 'express-promise-router'; -import express from 'express'; -import { - PreparerBuilder, - TemplaterBase, - JobProcessor, - RequiredTemplateValues, -} from '../scaffolder'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import Docker from 'dockerode'; -import { InputError } from '@backstage/backend-common'; -import { StageContext } from '../scaffolder/jobs/types'; -import { Octokit } from '@octokit/rest'; -import { GithubStorer } from '../scaffolder/stages/store/github'; 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 { + JobProcessor, + PreparerBuilder, + RequiredTemplateValues, + TemplaterBase, +} from '../scaffolder'; +import { StageContext } from '../scaffolder/jobs/types'; +import { GithubStorer } from '../scaffolder/stages/store/github'; + export interface RouterOptions { preparers: PreparerBuilder; templater: TemplaterBase; diff --git a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx index 4fb924e1f3..a2ee93c6f8 100644 --- a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx +++ b/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx @@ -25,6 +25,7 @@ import { Header, Lifecycle, InfoCard, + errorApiRef, } from '@backstage/core'; import { TemplateEntityV1alpha1, @@ -36,6 +37,7 @@ import { scaffolderApiRef } from '../../api'; import { MultistepJsonForm } from '../MultistepJsonForm'; import { Job } from '../JobStatusModal/types'; export const CreatePage = () => { + const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); @@ -68,16 +70,24 @@ export const CreatePage = () => { 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', - job.metadata.remoteUrl.replace( - /\.git$/, - '/blob/master/component-info.yaml', - ), - ); - setEntity(createdEntity); + } = await catalogApi.addLocation('github', componentYaml); + + setEntity(createdEntity as ComponentEntityV1alpha1); }; if (!template && isValidating) return ; diff --git a/plugins/scaffolder/src/components/JobStatusModal/types.ts b/plugins/scaffolder/src/components/JobStatusModal/types.ts index 2ace1f760f..7106444904 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/types.ts +++ b/plugins/scaffolder/src/components/JobStatusModal/types.ts @@ -1,23 +1,34 @@ -import { Writable } from 'stream'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; - +/* + * 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' | 'COMPLETE' | 'FAILED'; + status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; stages: Stage[]; - logStream?: Writable; - logger?: Logger; error?: Error; }; export type Stage = { name: string; log: string[]; - status: 'PENDING' | 'STARTED' | 'COMPLETE' | 'FAILED'; + status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; startedAt: string; - finishedAt?: string; + endedAt?: string; }; diff --git a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts index 89b7228a9c..4762838a9c 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts +++ b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts @@ -1,8 +1,24 @@ +/* + * 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 () => { @@ -17,12 +33,15 @@ const poll = (thunk: () => Promise, ms: number) => { }; }; -export const useJobPolling = (jobId: string | null) => { +export const useJobPolling = ( + jobId: string | null, + pollingInterval = DEFAULT_POLLING_INTERVAL, +) => { const scaffolderApi = useApi(scaffolderApiRef); + const [job, setJob] = useState(null); - const [job, setJob] = useState(undefined); useEffect(() => { - if (!jobId) return; + if (!jobId) return () => {}; const stopPolling = poll(async () => { const nextJobState = await scaffolderApi.getJob(jobId); if ( @@ -32,10 +51,11 @@ export const useJobPolling = (jobId: string | null) => { stopPolling(); } setJob(nextJobState); - }, 500); + }, pollingInterval); return () => { stopPolling(); }; - }, [jobId, setJob]); + }, [jobId, setJob, scaffolderApi, pollingInterval]); + return job; }; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 7e8f470fd9..03c513a437 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -1,5 +1,20 @@ +/* + * 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, { useState } from 'react'; -import { withTheme, FormProps, IChangeEvent } from '@rjsf/core'; +import { withTheme, IChangeEvent, FormProps } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import { Stepper, @@ -12,10 +27,11 @@ import { Box, } from '@material-ui/core'; import { Content, StructuredMetadataTable } from '@backstage/core'; +import { JSONSchema } from '@backstage/catalog-model'; const Form = withTheme(MuiTheme); type Step = { - schema: FormProps['schema']; + schema: JSONSchema; label: string; }; type Props = { @@ -52,7 +68,7 @@ export const MultistepJsonForm = ({ noHtml5Validate formData={formData} onChange={onChange} - schema={schema} + schema={schema as FormProps['schema']} onSubmit={e => { if (e.errors.length === 0) handleNext(); }} From 6de13cb54e8effd0e544f608a9410a947185f26d Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 1 Jul 2020 20:36:16 +0200 Subject: [PATCH 41/47] fix(scaffolder): big cleanup --- plugins/catalog/src/api/types.ts | 14 +- plugins/scaffolder/package.json | 6 +- .../src/components/CreatePage/index.ts | 1 - .../{JobStatusModal => JobStage}/JobStage.tsx | 79 ++++---- .../src/components/JobStage/index.ts | 16 ++ .../JobStatusModal/JobStatusModal.tsx | 25 ++- .../JobStatusModal/useJobPolling.ts | 3 +- .../MultistepJsonForm/MultistepJsonForm.tsx | 47 ++--- .../{index.tsx => ScaffolderPage.tsx} | 39 ++-- .../src/components/ScaffolderPage/index.ts | 16 ++ .../TemplateCard.tsx | 15 +- .../src/components/TemplateCard/index.ts | 17 ++ .../TemplatePage.tsx} | 170 +++++++++++------- .../src/components/TemplatePage/index.ts | 16 ++ plugins/scaffolder/src/index.ts | 2 +- plugins/scaffolder/src/plugin.ts | 6 +- plugins/scaffolder/src/routes.ts | 17 +- .../{components/JobStatusModal => }/types.ts | 0 yarn.lock | 7 +- 19 files changed, 318 insertions(+), 178 deletions(-) delete mode 100644 plugins/scaffolder/src/components/CreatePage/index.ts rename plugins/scaffolder/src/components/{JobStatusModal => JobStage}/JobStage.tsx (79%) create mode 100644 plugins/scaffolder/src/components/JobStage/index.ts rename plugins/scaffolder/src/components/ScaffolderPage/{index.tsx => ScaffolderPage.tsx} (84%) create mode 100644 plugins/scaffolder/src/components/ScaffolderPage/index.ts rename plugins/scaffolder/src/components/{ScaffolderPage => TemplateCard}/TemplateCard.tsx (87%) create mode 100644 plugins/scaffolder/src/components/TemplateCard/index.ts rename plugins/scaffolder/src/components/{CreatePage/CreatePage.tsx => TemplatePage/TemplatePage.tsx} (55%) create mode 100644 plugins/scaffolder/src/components/TemplatePage/index.ts rename plugins/scaffolder/src/{components/JobStatusModal => }/types.ts (100%) diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 3ac8a5d103..a523d97251 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -34,10 +34,18 @@ export interface CatalogApi { getEntityByName( compoundName: EntityCompoundName, ): Promise; - getEntities(filter?: Record): Promise; - addLocation(type: string, target: string): Promise; + getEntities( + filter?: Record, + ): Promise; + addLocation( + type: string, + target: string, + ): Promise>; getLocationByEntity(entity: Entity): Promise; removeEntityByUid(uid: string): Promise; } -export type AddLocationResponse = { location: Location; entities: Entity[] }; +export type AddLocationResponse = { + location: Location; + entities: EntityType[]; +}; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 9dfdde2397..312e90169f 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -25,15 +25,17 @@ "@backstage/core": "^0.1.1-alpha.12", "@backstage/plugin-catalog": "^0.1.1-alpha.12", "@backstage/theme": "^0.1.1-alpha.12", - "@backstage/catalog-model": "^0.1.1-alpha.12", - "@backstage/plugin-catalog": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@rjsf/core": "^2.1.0", "@rjsf/material-ui": "^2.1.0", + "classnames": "^2.2.6", + "moment": "^2.27.0", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-lazylog": "^4.5.2", + "react-router": "^5.2.0", "react-router-dom": "6.0.0-alpha.5", "react-use": "^14.2.0", "swr": "^0.2.2" diff --git a/plugins/scaffolder/src/components/CreatePage/index.ts b/plugins/scaffolder/src/components/CreatePage/index.ts deleted file mode 100644 index e1be7978a9..0000000000 --- a/plugins/scaffolder/src/components/CreatePage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CreatePage } from './CreatePage'; diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx b/plugins/scaffolder/src/components/JobStage/JobStage.tsx similarity index 79% rename from plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx rename to plugins/scaffolder/src/components/JobStage/JobStage.tsx index ef799d13b4..616e350ed4 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStage.tsx +++ b/plugins/scaffolder/src/components/JobStage/JobStage.tsx @@ -13,21 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect, useState, Suspense } from 'react'; import { + Box, ExpansionPanel, - ExpansionPanelSummary, - Typography, ExpansionPanelDetails, + ExpansionPanelSummary, LinearProgress, + Typography, } from '@material-ui/core'; -import moment from 'moment'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { makeStyles } from '@material-ui/core/styles'; -import { Job } from './types'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import cn from 'classnames'; +import moment from 'moment'; +import React, { Suspense, useEffect, useState } from 'react'; +import { Job } from '../../types'; const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); moment.relativeTimeThreshold('ss', 0); + const useStyles = makeStyles(theme => ({ expansionPanelDetails: { padding: 0, @@ -37,37 +40,10 @@ const useStyles = makeStyles(theme => ({ marginRight: 0, marginLeft: '-20px', }, - neutral: {}, - failed: { - position: 'relative', - '&:after': { - pointerEvents: 'none', - content: '""', - position: 'absolute', - top: 0, - right: 0, - left: 0, - bottom: 0, - boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`, - }, - }, - running: { - position: 'relative', - '&:after': { - pointerEvents: 'none', - content: '""', - position: 'absolute', - top: 0, - right: 0, - left: 0, - bottom: 0, - boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`, - }, - }, cardContent: { backgroundColor: theme.palette.background.default, }, - success: { + expansionPanel: { position: 'relative', '&:after': { pointerEvents: 'none', @@ -77,6 +53,21 @@ const useStyles = makeStyles(theme => ({ right: 0, left: 0, bottom: 0, + }, + }, + neutral: {}, + failed: { + '&:after': { + boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`, + }, + }, + started: { + '&:after': { + boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`, + }, + }, + completed: { + '&:after': { boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`, }, }, @@ -90,13 +81,14 @@ type Props = { endedAt?: string; status?: Job['status']; }; + export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { const classes = useStyles(); const [expanded, setExpanded] = useState(false); useEffect(() => { if (status === 'FAILED') setExpanded(true); - }, [status === 'FAILED', setExpanded]); + }, [status, setExpanded]); const timeElapsed = status !== 'PENDING' @@ -108,15 +100,10 @@ export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { return ( setExpanded(newState)} > @@ -134,11 +121,11 @@ export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { {log.length === 0 ? ( - 'Nothing here...' + 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 index f795444493..a129d7c26a 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import React, { useEffect } from 'react'; import { Dialog, @@ -6,13 +21,14 @@ import { DialogContent, DialogActions, } from '@material-ui/core'; -import { JobStage } from './JobStage'; +import { JobStage } from '../JobStage/JobStage'; import { useJobPolling } from './useJobPolling'; -import { Job } from './types'; +import { Job } from '../../types'; import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; import { Button } from '@backstage/core'; import { entityRoute } from '@backstage/plugin-catalog'; import { generatePath } from 'react-router-dom'; + type Props = { onClose: () => void; onComplete: (job: Job) => void; @@ -29,8 +45,9 @@ export const JobStatusModal = ({ const job = useJobPolling(jobId); useEffect(() => { - if (job?.status === 'COMPLETED') onComplete(job as Job); - }, [job]); + if (job?.status === 'COMPLETED') onComplete(job); + }, [job, onComplete]); + return ( diff --git a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts index 4762838a9c..9239864c03 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts +++ b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { useState, useEffect } from 'react'; -import { Job } from './types'; +import { Job } from '../../types'; import { useApi } from '@backstage/core'; import { scaffolderApiRef } from '../../api'; @@ -42,6 +42,7 @@ export const useJobPolling = ( useEffect(() => { if (!jobId) return () => {}; + const stopPolling = poll(async () => { const nextJobState = await scaffolderApi.getJob(jobId); if ( diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 03c513a437..81c8abc2fb 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -13,34 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; -import { withTheme, IChangeEvent, FormProps } from '@rjsf/core'; -import { Theme as MuiTheme } from '@rjsf/material-ui'; +import { JSONSchema } from '@backstage/catalog-model'; +import { Content, StructuredMetadataTable } from '@backstage/core'; import { - Stepper, - Step, - StepLabel, - StepContent, + Box, Button, Paper, + Step, + StepContent, + StepLabel, + Stepper, Typography, - Box, } from '@material-ui/core'; -import { Content, StructuredMetadataTable } from '@backstage/core'; -import { JSONSchema } from '@backstage/catalog-model'; +import { FormProps, IChangeEvent, withTheme } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; +import React, { useState } from 'react'; const Form = withTheme(MuiTheme); type Step = { schema: JSONSchema; label: string; -}; +} & Partial; + 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, @@ -57,10 +62,11 @@ export const MultistepJsonForm = ({ const handleNext = () => setActiveStep(Math.min(activeStep + 1, steps.length)); const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0)); + return ( <> - {steps.map(({ label, schema }) => ( + {steps.map(({ label, schema, ...formProps }) => ( {label} @@ -72,17 +78,14 @@ export const MultistepJsonForm = ({ onSubmit={e => { if (e.errors.length === 0) handleNext(); }} + {...formProps} > -
-
- - -
-
+ +
diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx similarity index 84% rename from plugins/scaffolder/src/components/ScaffolderPage/index.tsx rename to plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 1570006b44..ac13634842 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,31 +14,43 @@ * limitations under the License. */ -import React, { useEffect } from 'react'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { - Lifecycle, Content, ContentHeader, + errorApiRef, Header, - SupportButton, + Lifecycle, Page, pageTheme, + SupportButton, useApi, - errorApiRef, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; import { - Typography, - Link, Button, Grid, LinearProgress, + Link, + Typography, } from '@material-ui/core'; +import React, { useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import TemplateCard from './TemplateCard'; import useStaleWhileRevalidate from 'swr'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { TemplateCard, TemplateCardProps } from '../TemplateCard'; +const getTemplateCardProps = ( + template: TemplateEntityV1alpha1, +): TemplateCardProps & { key: string } => { + return { + key: template.metadata.uid!, + name: template.metadata.name, + title: `${(template.metadata.title || template.metadata.name) ?? ''}`, + type: template.spec.type ?? '', + description: template.metadata.description ?? '-', + tags: (template.metadata?.tags as string[]) ?? [], + }; +}; export const ScaffolderPage: React.FC<{}> = () => { const catalogApi = useApi(catalogApiRef); const errorApi = useApi(errorApiRef); @@ -96,16 +108,7 @@ export const ScaffolderPage: React.FC<{}> = () => { templates.map(template => { return ( - + ); })} 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/ScaffolderPage/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx similarity index 87% rename from plugins/scaffolder/src/components/ScaffolderPage/TemplateCard.tsx rename to plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 263bc3846b..2f36e12511 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Button } from '@backstage/core'; +import { Card, Chip, makeStyles, Typography } from '@material-ui/core'; import React from 'react'; import { generatePath } from 'react-router-dom'; -import { Button } from '@backstage/core'; -import { Card, Chip, Typography, makeStyles } from '@material-ui/core'; -import { createTemplateRoute } from '../../routes'; +import { templateRoute } from '../../routes'; const useStyles = makeStyles(theme => ({ header: { @@ -40,14 +40,15 @@ const useStyles = makeStyles(theme => ({ }, })); -type TemplateCardProps = { +export type TemplateCardProps = { description: string; tags: string[]; title: string; type: string; name: string; }; -const TemplateCard = ({ + +export const TemplateCard = ({ description, tags, title, @@ -55,7 +56,7 @@ const TemplateCard = ({ name, }: TemplateCardProps) => { const classes = useStyles(); - const href = generatePath(createTemplateRoute.path, { templateName: name }); + const href = generatePath(templateRoute.path, { templateName: name }); return ( @@ -79,5 +80,3 @@ const TemplateCard = ({ ); }; - -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/CreatePage/CreatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx similarity index 55% rename from plugins/scaffolder/src/components/CreatePage/CreatePage.tsx rename to plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index a2ee93c6f8..5984d6f1c2 100644 --- a/plugins/scaffolder/src/components/CreatePage/CreatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -13,45 +13,76 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; -import useStaleWhileRevalidate from 'swr'; -import { useParams } from 'react-router-dom'; -import { LinearProgress } from '@material-ui/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; import { - useApi, - Page, - Content, - Header, - Lifecycle, - InfoCard, - errorApiRef, -} from '@backstage/core'; -import { - TemplateEntityV1alpha1, ComponentEntityV1alpha1, + TemplateEntityV1alpha1, } from '@backstage/catalog-model'; +import { + Content, + errorApiRef, + Header, + InfoCard, + Lifecycle, + Page, + useApi, +} from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { LinearProgress } from '@material-ui/core'; import { IChangeEvent } from '@rjsf/core'; -import { JobStatusModal } from '../JobStatusModal'; +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import useStaleWhileRevalidate from 'swr'; import { scaffolderApiRef } from '../../api'; +import { JobStatusModal } from '../JobStatusModal'; +import { Job } from '../../types'; import { MultistepJsonForm } from '../MultistepJsonForm'; -import { Job } from '../JobStatusModal/types'; -export const CreatePage = () => { +import { Navigate } from 'react-router'; +import { rootRoute } from '../../routes'; + +const useTemplate = ( + templateName: string, + catalogApi: typeof catalogApiRef.T, +) => { + const { data, error } = useStaleWhileRevalidate( + `templates/${templateName}`, + async () => + catalogApi.getEntities({ + kind: 'Template', + 'metadata.name': templateName, + }), + ); + return { template: data?.[0], loading: !error && !data, error }; +}; + +const OWNER_REPO_SCHEMA = { + $schema: 'http://json-schema.org/draft-07/schema#' as const, + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string' as const, + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + format: 'GitHub user or org / Repo name', + type: 'string' as const, + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, +}; + +const REPO_FORMAT = { + 'GitHub user or org / Repo name': /[^\/]*\/[^\/]*/, +}; + +export const TemplatePage = () => { const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); - const { - data: [template] = [] as TemplateEntityV1alpha1[], - isValidating, - } = useStaleWhileRevalidate( - `templates/${templateName}`, - async () => - (catalogApi.getEntities({ - kind: 'Template', - 'metadata.name': templateName, - }) as any) as Promise, - ); + const { template, loading } = useTemplate(templateName, catalogApi); + const [formState, setFormState] = useState({}); const handleFormReset = () => setFormState({}); @@ -62,22 +93,24 @@ export const CreatePage = () => { const handleClose = () => setJobId(null); const handleCreate = async () => { - const job = await scaffolderApi.scaffold(template, formState); + const job = await scaffolderApi.scaffold(template!, formState); setJobId(job); }; const [entity, setEntity] = React.useState( null, ); + const handleCreateComplete = async (job: Job) => { const componentYaml = job.metadata.remoteUrl?.replace( /\.git$/, '/blob/master/component-info.yaml', ); + if (!componentYaml) { errorApi.post( new Error( - `Failed to find component-info.yaml file in ${job.metadata.remoteUrl}`, + `Failed to find component-info.yaml file in ${job.metadata.remoteUrl}.`, ), ); return; @@ -85,13 +118,27 @@ export const CreatePage = () => { const { entities: [createdEntity], - } = await catalogApi.addLocation('github', componentYaml); + } = await catalogApi.addLocation( + 'github', + componentYaml, + ); - setEntity(createdEntity as ComponentEntityV1alpha1); + setEntity(createdEntity); }; - if (!template && isValidating) return ; - if (!template || !template?.spec?.schema) return null; + 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 ( @@ -105,6 +152,7 @@ export const CreatePage = () => { subtitle="Create new software components using standard templates" /> + {loading && } {jobId && ( { entity={entity} /> )} - - + - + { + label: 'Choose owner and repo', + schema: OWNER_REPO_SCHEMA, + customFormats: REPO_FORMAT, + }, + ]} + /> + + )} ); 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 3fd7d38c57..4a1c6ce6a5 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -15,4 +15,4 @@ */ export { plugin } from './plugin'; -export { rootRoute, createTemplateRoute } from './routes'; +export { rootRoute, templateRoute } from './routes'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index d0d38d5812..8c4ed5badd 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -16,13 +16,13 @@ import { createPlugin } from '@backstage/core'; import { ScaffolderPage } from './components/ScaffolderPage'; -import { CreatePage } from './components/CreatePage'; -import { rootRoute, createTemplateRoute } from './routes'; +import { TemplatePage } from './components/TemplatePage'; +import { rootRoute, templateRoute } from './routes'; export const plugin = createPlugin({ id: 'scaffolder', register({ router }) { router.addRoute(rootRoute, ScaffolderPage); - router.addRoute(createTemplateRoute, CreatePage); + router.addRoute(templateRoute, TemplatePage); }, }); diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index f42f9540f9..28c77f29a6 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { createRouteRef } from '@backstage/core'; export const rootRoute = createRouteRef({ @@ -5,7 +20,7 @@ export const rootRoute = createRouteRef({ path: '/create', title: 'Create new entity', }); -export const createTemplateRoute = createRouteRef({ +export const templateRoute = createRouteRef({ icon: () => null, path: '/create/:templateName', title: 'Entity creation', diff --git a/plugins/scaffolder/src/components/JobStatusModal/types.ts b/plugins/scaffolder/src/types.ts similarity index 100% rename from plugins/scaffolder/src/components/JobStatusModal/types.ts rename to plugins/scaffolder/src/types.ts diff --git a/yarn.lock b/yarn.lock index 8b28254a52..2a86168c2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13030,6 +13030,11 @@ moment@2.26.0, moment@^2.25.3, moment@^2.26.0: resolved "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== +moment@^2.27.0: + version "2.27.0" + resolved "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + monaco-editor@^0.20.0: version "0.20.0" resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea" @@ -15697,7 +15702,7 @@ react-router-dom@^5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.2.0: +react-router@5.2.0, react-router@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== From 6aee52bd8c266da0715e4560e008affc19edc420 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 1 Jul 2020 23:24:48 +0200 Subject: [PATCH 42/47] fix(scaffolder): more fixes --- plugins/scaffolder/package.json | 2 +- .../src/components/JobStage/JobStage.tsx | 5 +- .../MultistepJsonForm/MultistepJsonForm.tsx | 2 +- plugins/scaffolder/src/index.ts | 1 + yarn.lock | 68 ++----------------- 5 files changed, 11 insertions(+), 67 deletions(-) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 312e90169f..79c9b36143 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -35,7 +35,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", - "react-router": "^5.2.0", + "react-router": "6.0.0-alpha.5", "react-router-dom": "6.0.0-alpha.5", "react-use": "^14.2.0", "swr": "^0.2.2" diff --git a/plugins/scaffolder/src/components/JobStage/JobStage.tsx b/plugins/scaffolder/src/components/JobStage/JobStage.tsx index 616e350ed4..71b73b752c 100644 --- a/plugins/scaffolder/src/components/JobStage/JobStage.tsx +++ b/plugins/scaffolder/src/components/JobStage/JobStage.tsx @@ -79,7 +79,7 @@ type Props = { log: string[]; startedAt: string; endedAt?: string; - status?: Job['status']; + status: Job['status']; }; export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { @@ -102,7 +102,8 @@ export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { TransitionProps={{ unmountOnExit: true }} className={cn( classes.expansionPanel, - classes[status.toLowerCase()] ?? classes.neutral, + classes[status.toLowerCase() as keyof ReturnType] ?? + classes.neutral, )} expanded={expanded} onChange={(_, newState) => setExpanded(newState)} diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 81c8abc2fb..f0ba720b39 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -33,7 +33,7 @@ const Form = withTheme(MuiTheme); type Step = { schema: JSONSchema; label: string; -} & Partial; +} & Partial, 'schema'>>; type Props = { /** diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 4a1c6ce6a5..f0fdf5b429 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -15,4 +15,5 @@ */ export { plugin } from './plugin'; +export { ScaffolderApi, scaffolderApiRef } from './api'; export { rootRoute, templateRoute } from './routes'; diff --git a/yarn.lock b/yarn.lock index c790e45998..db00142d54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9912,18 +9912,6 @@ history@5.0.0-beta.9: dependencies: "@babel/runtime" "^7.7.6" -history@^4.9.0: - version "4.10.1" - resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -9933,7 +9921,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -11056,11 +11044,6 @@ is-yarn-global@^0.3.0: resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -12408,7 +12391,7 @@ loglevel@^1.6.8: resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -12850,14 +12833,6 @@ min-indent@^1.0.0: resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= -mini-create-react-context@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040" - integrity sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA== - dependencies: - "@babel/runtime" "^7.5.5" - tiny-warning "^1.0.3" - mini-css-extract-plugin@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0" @@ -14338,13 +14313,6 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-type@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -15617,7 +15585,7 @@ react-inspector@^4.0.0: is-dom "^1.0.9" prop-types "^15.6.1" -react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -15704,22 +15672,6 @@ react-router@6.0.0-alpha.5, react-router@^6.0.0-alpha.5: history "5.0.0-beta.9" prop-types "^15.7.2" -react-router@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" - integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== - dependencies: - "@babel/runtime" "^7.1.2" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - mini-create-react-context "^0.4.0" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - react-side-effect@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz#1ce4a8b4445168c487ed24dab886421f74d380d3" @@ -16330,11 +16282,6 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -18146,12 +18093,12 @@ tiny-emitter@^2.0.0: resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== -tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: +tiny-invariant@^1.0.6: version "1.1.0" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== -tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: +tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -18943,11 +18890,6 @@ validate.io-number@^1.0.3: resolved "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz#f63ffeda248bf28a67a8d48e0e3b461a1665baf8" integrity sha1-9j/+2iSL8opnqNSODjtGGhZluvg= -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From fbcc0db2dbd1ea0f278387406276cddb8339694a Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 1 Jul 2020 23:26:45 +0200 Subject: [PATCH 43/47] test(scaffolder): add tests for TemplatePage --- plugins/catalog/src/api/CatalogClient.ts | 4 +- plugins/catalog/src/api/types.ts | 13 +- plugins/scaffolder/package.json | 1 + .../JobStatusModal/JobStatusModal.tsx | 4 +- .../src/components/JobStatusModal/index.ts | 15 ++ .../src/components/MultistepJsonForm/index.ts | 15 ++ .../TemplatePage/TemplatePage.test.tsx | 154 ++++++++++++++++++ .../components/TemplatePage/TemplatePage.tsx | 20 +-- 8 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 65ee8a065e..3804315ace 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -70,9 +70,9 @@ export class CatalogClient implements CatalogApi { return await this.getOptional(`/locations/${id}`); } - async getEntities( + async getEntities( filter?: Record, - ): Promise { + ): Promise { let path = `/entities`; if (filter) { const params = new URLSearchParams(); diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index a523d97251..36c86f2e96 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -34,18 +34,13 @@ export interface CatalogApi { getEntityByName( compoundName: EntityCompoundName, ): Promise; - getEntities( - filter?: Record, - ): Promise; - addLocation( - type: string, - target: string, - ): Promise>; + getEntities(filter?: Record): Promise; + addLocation(type: string, target: string): Promise; getLocationByEntity(entity: Entity): Promise; removeEntityByUid(uid: string): Promise; } -export type AddLocationResponse = { +export type AddLocationResponse = { location: Location; - entities: EntityType[]; + entities: Entity[]; }; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 79c9b36143..403c9fed59 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -43,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/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx index a129d7c26a..c6ed55c92c 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -24,7 +24,7 @@ import { import { JobStage } from '../JobStage/JobStage'; import { useJobPolling } from './useJobPolling'; import { Job } from '../../types'; -import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Button } from '@backstage/core'; import { entityRoute } from '@backstage/plugin-catalog'; import { generatePath } from 'react-router-dom'; @@ -33,7 +33,7 @@ type Props = { onClose: () => void; onComplete: (job: Job) => void; jobId: string; - entity: ComponentEntityV1alpha1 | null; + entity: TemplateEntityV1alpha1 | null; }; export const JobStatusModal = ({ diff --git a/plugins/scaffolder/src/components/JobStatusModal/index.ts b/plugins/scaffolder/src/components/JobStatusModal/index.ts index 393cc8bfaf..5598999fe3 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/index.ts +++ b/plugins/scaffolder/src/components/JobStatusModal/index.ts @@ -1 +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/MultistepJsonForm/index.ts b/plugins/scaffolder/src/components/MultistepJsonForm/index.ts index 6cb93273a6..fa28c5803b 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/index.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/index.ts @@ -1 +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/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 index 5984d6f1c2..5c949e55d9 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - ComponentEntityV1alpha1, - TemplateEntityV1alpha1, -} from '@backstage/catalog-model'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Content, errorApiRef, @@ -46,10 +43,10 @@ const useTemplate = ( const { data, error } = useStaleWhileRevalidate( `templates/${templateName}`, async () => - catalogApi.getEntities({ + catalogApi.getEntities({ kind: 'Template', 'metadata.name': templateName, - }), + }) as Promise, ); return { template: data?.[0], loading: !error && !data, error }; }; @@ -97,7 +94,7 @@ export const TemplatePage = () => { setJobId(job); }; - const [entity, setEntity] = React.useState( + const [entity, setEntity] = React.useState( null, ); @@ -118,12 +115,9 @@ export const TemplatePage = () => { const { entities: [createdEntity], - } = await catalogApi.addLocation( - 'github', - componentYaml, - ); + } = await catalogApi.addLocation('github', componentYaml); - setEntity(createdEntity); + setEntity((createdEntity as any) as TemplateEntityV1alpha1); }; if (!loading && !template) { @@ -152,7 +146,7 @@ export const TemplatePage = () => { subtitle="Create new software components using standard templates" /> - {loading && } + {loading && } {jobId && ( Date: Thu, 2 Jul 2020 00:28:14 +0200 Subject: [PATCH 44/47] fix: moment version --- plugins/scaffolder/package.json | 2 +- yarn.lock | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 403c9fed59..25121b29e8 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -31,7 +31,7 @@ "@rjsf/core": "^2.1.0", "@rjsf/material-ui": "^2.1.0", "classnames": "^2.2.6", - "moment": "^2.27.0", + "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", diff --git a/yarn.lock b/yarn.lock index db00142d54..aafe42a250 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13005,11 +13005,6 @@ moment@2.26.0, moment@^2.25.3, moment@^2.26.0: resolved "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== -moment@^2.27.0: - version "2.27.0" - resolved "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" - integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== - monaco-editor@^0.20.0: version "0.20.0" resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea" From 31257122d223eee0fa008f078f65199e49e066cd Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 2 Jul 2020 01:02:17 +0200 Subject: [PATCH 45/47] fix: types --- packages/catalog-model/package.json | 1 + yarn.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 64ac3b3221..b51dacde84 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -21,6 +21,7 @@ }, "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", diff --git a/yarn.lock b/yarn.lock index aafe42a250..30eca40054 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3675,7 +3675,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== From 0933734e0876edd19b80de9ff8fd66094536b8dc Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 7 Jul 2020 09:08:30 +0200 Subject: [PATCH 46/47] fix(scaffolder): get correct resultDir, template --- .../springboot-template/template.yaml | 13 +++++++++++++ plugins/scaffolder-backend/src/service/router.ts | 2 +- plugins/scaffolder/src/api.ts | 3 ++- 3 files changed, 16 insertions(+), 2 deletions(-) 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/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 888bce0d2a..abe12c94eb 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -106,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/src/api.ts b/plugins/scaffolder/src/api.ts index 585a49ad78..0e4df2fd41 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -52,7 +52,8 @@ export class ScaffolderApi { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ template, values }), + // TODO(shmidt-i): when repo picker is implemented, take isOrg from it + body: JSON.stringify({ template, ...values, isOrg: true }), }); if (response.status !== 201) { From 7e38251cef9934f45637f9e42db5b01d7586a7d1 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 7 Jul 2020 09:40:42 +0200 Subject: [PATCH 47/47] fix(scaffloder): pass values correctly --- plugins/scaffolder/src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 0e4df2fd41..4a0d508a2b 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -53,7 +53,7 @@ export class ScaffolderApi { 'Content-Type': 'application/json', }, // TODO(shmidt-i): when repo picker is implemented, take isOrg from it - body: JSON.stringify({ template, ...values, isOrg: true }), + body: JSON.stringify({ template, values: { ...values, isOrg: true } }), }); if (response.status !== 201) {