diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index 4ebb2edc79..fad4ae34f7 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -16,12 +16,5 @@ export { HigherOrderOperations } from './HigherOrderOperations'; export { LocationReaders } from './LocationReaders'; -export type { - AddLocationResult, - HigherOrderOperation, - LocationReader, - ReadLocationEntity, - ReadLocationError, - ReadLocationResult, -} from './types'; +export * from './types'; export * from './processors'; diff --git a/plugins/import-component/.eslintrc.js b/plugins/import-component/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/import-component/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/import-component/README.md b/plugins/import-component/README.md new file mode 100644 index 0000000000..682212d98d --- /dev/null +++ b/plugins/import-component/README.md @@ -0,0 +1,7 @@ +# Register component plugin + +Welcome to the import-component plugin! + +This plugin allows you to create a component-config YAML file for your repository. + +When installed it is accessible on [localhost:3000/import-component](localhost:3000/import-component). diff --git a/plugins/import-component/dev/index.tsx b/plugins/import-component/dev/index.tsx new file mode 100644 index 0000000000..d97643057b --- /dev/null +++ b/plugins/import-component/dev/index.tsx @@ -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. + */ + +import { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp() + .registerPlugin(plugin) + .render(); diff --git a/plugins/import-component/package.json b/plugins/import-component/package.json new file mode 100644 index 0000000000..73a9aa007f --- /dev/null +++ b/plugins/import-component/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-import-component", + "version": "0.1.1-alpha.26", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1-alpha.26", + "@backstage/plugin-catalog": "^0.1.1-alpha.26", + "@backstage/plugin-catalog-backend": "^0.1.1-alpha.26", + "@backstage/theme": "^0.1.1-alpha.26", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@octokit/rest": "^18.0.6", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-hook-form": "^6.6.0", + "react-router": "^5.2.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^15.3.3", + "yaml": "^1.10.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/dev-utils": "^0.1.1-alpha.26", + "@backstage/test-utils": "^0.1.1-alpha.26", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/import-component/src/api/CatalogImportApi.ts b/plugins/import-component/src/api/CatalogImportApi.ts new file mode 100644 index 0000000000..58d1f87f00 --- /dev/null +++ b/plugins/import-component/src/api/CatalogImportApi.ts @@ -0,0 +1,56 @@ +/* + * 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. + */ +/* + * Copyright 2020 Roadie 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 { Entity } from '@backstage/catalog-model'; +import { RecursivePartial } from '../util/types'; + +export const catalogImportApiRef = createApiRef({ + id: 'plugin.catalogimport.service', + description: 'Used by the catalog import plugin to make requests', +}); + +export interface CatalogImportApi { + submitPRToRepo(options: { + token: string; + owner: string; + repo: string; + fileContent: string; + }): Promise<{ errorMessage: string | null; link: string }>; + createRepositoryLocation(options: { + token: string; + owner: string; + repo: string; + }): Promise<{ errorMessage: string | null }>; + generateEntityDefinitions(options: { + repo: string; + }): Promise[]>; +} diff --git a/plugins/import-component/src/api/CatalogImportClient.ts b/plugins/import-component/src/api/CatalogImportClient.ts new file mode 100644 index 0000000000..d67febe2c8 --- /dev/null +++ b/plugins/import-component/src/api/CatalogImportClient.ts @@ -0,0 +1,135 @@ +/* + * 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. + */ +/* + * Copyright 2020 Roadie 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 { Octokit } from '@octokit/rest'; +import { Entity } from '@backstage/catalog-model'; +import { DiscoveryApi } from '@backstage/core'; +import { CatalogImportApi } from './CatalogImportApi'; +import { AnalyzeLocationResponse } from '@backstage/plugin-catalog-backend'; +import { RecursivePartial } from '../util/types'; + +export const API_BASE_URL = '/api/catalog/locations'; + +export class CatalogImportClient implements CatalogImportApi { + private readonly discoveryApi: DiscoveryApi; + + constructor(options: { discoveryApi: DiscoveryApi }) { + this.discoveryApi = options.discoveryApi; + } + + async generateEntityDefinitions({ + repo, + }: { + repo: string; + }): Promise[]> { + const response = await fetch( + `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`, + { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + location: { type: 'github', target: repo }, + }), + }, + ); + const payload = (await response.json()) as AnalyzeLocationResponse; + return payload.generateEntities.map(x => x.entity); + } + + async createRepositoryLocation({ + owner, + repo, + }: { + token: string; + owner: string; + repo: string; + }): Promise<{ errorMessage: string | null }> { + await fetch(`${await this.discoveryApi.getBaseUrl('catalog')}/locations`, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + type: 'github', + target: `https://github.com/${owner}/${repo}/blob/master/catalog-info.yaml`, + presence: 'optional', + }), + }); + return { errorMessage: null }; + } + + async submitPRToRepo({ + token, + owner, + repo, + fileContent, + }: { + token: string; + owner: string; + repo: string; + fileContent: string; + }): Promise<{ errorMessage: string | null; link: string }> { + const octo = new Octokit({ + auth: token, + }); + + const parentRef = await octo.git.getRef({ + owner, + repo, + ref: 'heads/master', + }); + await octo.git.createRef({ + owner, + repo, + ref: 'refs/heads/backstage-integration', + sha: parentRef.data.object.sha, + }); + await octo.repos.createOrUpdateFileContents({ + owner, + repo, + path: 'backstage.yaml', + message: 'Add backstage.yaml config file', + content: btoa(fileContent), + branch: 'backstage-integration', + }); + const pullRequestRespone = await octo.pulls.create({ + owner, + repo, + title: 'Add backstage.yaml config file', + head: 'backstage-integration', + base: 'master', + }); + + return { errorMessage: null, link: pullRequestRespone.data.html_url }; + } +} diff --git a/plugins/import-component/src/api/index.ts b/plugins/import-component/src/api/index.ts new file mode 100644 index 0000000000..9ebc110843 --- /dev/null +++ b/plugins/import-component/src/api/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Copyright 2020 Roadie 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 './CatalogImportApi'; +export * from './CatalogImportClient'; +export * from './types'; diff --git a/plugins/import-component/src/api/types.ts b/plugins/import-component/src/api/types.ts new file mode 100644 index 0000000000..c18d0cf547 --- /dev/null +++ b/plugins/import-component/src/api/types.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ +/* + * Copyright 2020 Roadie 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 GithubRepoDto = { + full_name: string; + private: boolean; + description: string; + html_url: string; + language: string; +}; diff --git a/plugins/import-component/src/components/ImportComponentForm/ImportComponentForm.tsx b/plugins/import-component/src/components/ImportComponentForm/ImportComponentForm.tsx new file mode 100644 index 0000000000..c28e5889ac --- /dev/null +++ b/plugins/import-component/src/components/ImportComponentForm/ImportComponentForm.tsx @@ -0,0 +1,170 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { errorApiRef, useApi } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; +import { + Button, + FormControl, + FormHelperText, + InputLabel, + MenuItem, + Select, + TextField, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useMountedState } from 'react-use'; +import { RecursivePartial } from '../../util/types'; +import { ComponentIdValidators } from '../../util/validate'; +import { useGithubRepos } from '../ImportComponentPage/useGithubRepos'; + +const useStyles = makeStyles(theme => ({ + form: { + alignItems: 'flex-start', + display: 'flex', + flexFlow: 'column nowrap', + }, + submit: { + marginTop: theme.spacing(1), + }, + select: { + minWidth: 120, + }, +})); + +type Props = { + nextStep: () => void; + saveConfig: (configFile: { + repo: string; + config: RecursivePartial[]; + }) => void; +}; + +export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => { + const { control, register, handleSubmit, errors, formState } = useForm({ + mode: 'onChange', + }); + const classes = useStyles(); + const hasErrors = !!errors.componentLocation; + const dirty = formState?.isDirty; + + const isMounted = useMountedState(); + const errorApi = useApi(errorApiRef); + const { generateEntityDefinitions } = useGithubRepos(); + + const onSubmit = async (formData: Record) => { + const { componentLocation: target } = formData; + try { + // const typeMapping = [ + // { url: /^https:\/\/gitlab\.com\/.*/, type: 'gitlab/api' }, + // { url: /^https:\/\/bitbucket\.org\/.*/, type: 'bitbucket/api' }, + // { url: /^https:\/\/dev\.azure\.com\/.*/, type: 'azure/api' }, + // { url: /.*/, type: 'github' }, + // ]; + + // const type = + // scmType === 'AUTO' + // ? typeMapping.filter(item => item.url.test(target))[0].type + // : scmType; + + if (!isMounted()) return; + + const repo = target + .split('/') + .slice(-2) + .join('/'); + + const config = await generateEntityDefinitions(repo); + saveConfig({ + repo, + config, + }); + nextStep(); + } catch (e) { + errorApi.post(e); + } + }; + + return ( +
+ + + + {errors.componentLocation && ( + + {errors.componentLocation.message} + + )} + + + + Host type + ( + + )} + /> + + +
+ ); +}; diff --git a/plugins/import-component/src/components/ImportComponentForm/index.ts b/plugins/import-component/src/components/ImportComponentForm/index.ts new file mode 100644 index 0000000000..ed118f5412 --- /dev/null +++ b/plugins/import-component/src/components/ImportComponentForm/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 * from './ImportComponentForm'; diff --git a/plugins/import-component/src/components/ImportComponentPage/ComponentConfigDisplay.tsx b/plugins/import-component/src/components/ImportComponentPage/ComponentConfigDisplay.tsx new file mode 100644 index 0000000000..22d3dac322 --- /dev/null +++ b/plugins/import-component/src/components/ImportComponentPage/ComponentConfigDisplay.tsx @@ -0,0 +1,98 @@ +/* + * 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. + */ +/* + * Copyright 2020 RoadieHQ + * + * 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, { useCallback, useState } from 'react'; +import { Button, CircularProgress, Grid, Tooltip } from '@material-ui/core'; +import Alert from '@material-ui/lab/Alert'; +import { useGithubRepos } from './useGithubRepos'; +import { Entity } from '@backstage/catalog-model'; +import { RecursivePartial } from '../../util/types'; + +type Props = { + nextStep: () => void; + configFile: { repo: string; config: RecursivePartial[] }; + savePRLink: (PRLink: string) => void; +}; + +const ComponentConfigDisplay: React.FC = ({ + nextStep, + configFile, + savePRLink, +}) => { + const [submitting, setSubmitting] = useState(false); + const { submitPRToRepo } = useGithubRepos(); + const onNext = useCallback(async () => { + setSubmitting(true); + const result = await submitPRToRepo(configFile); + savePRLink(result.link); + setSubmitting(false); + nextStep(); + }, [submitPRToRepo, configFile, nextStep, savePRLink]); + + return ( + + + + config file has been generated. You can optionally edit it before + submitting Pull Request + + + + + + + + + + + {submitting ? ( + + + + ) : null} + + + + + + ); +}; + +export default ComponentConfigDisplay; diff --git a/plugins/import-component/src/components/ImportComponentPage/ImportComponentPage.tsx b/plugins/import-component/src/components/ImportComponentPage/ImportComponentPage.tsx new file mode 100644 index 0000000000..719d5e93f4 --- /dev/null +++ b/plugins/import-component/src/components/ImportComponentPage/ImportComponentPage.tsx @@ -0,0 +1,95 @@ +/* + * 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 { Grid } from '@material-ui/core'; +import { + InfoCard, + Page, + Content, + Header, + SupportButton, + ContentHeader, +} from '@backstage/core'; +import { RegisterComponentForm } from '../ImportComponentForm'; +import { Entity } from '@backstage/catalog-model'; +import ImportStepper from './ImportStepper'; +import ComponentConfigDisplay from './ComponentConfigDisplay'; +import { ImportFinished } from './ImportFinished'; +import { RecursivePartial } from '../../util/types'; + +export const ImportComponentPage = () => { + const [activeStep, setActiveStep] = useState(0); + const [configFile, setConfigFile] = useState<{ + repo: string; + config: RecursivePartial[]; + }>({ repo: '', config: [] }); + const [PRLink, setPRLink] = useState(''); + const nextStep = (options?: { reset: boolean }) => { + setActiveStep(step => (options?.reset ? 0 : step + 1)); + }; + + return ( + +
+ + + + Generate a component definition file and automatically submit it as + a pull request. TODO: Add more information about what this is. + + + + + + + ), + }, + { + step: 'Review generated component config files', + content: ( + + ), + }, + { + step: 'Finish', + content: ( + + ), + }, + ]} + activeStep={activeStep} + nextStep={nextStep} + /> + + + + + + ); +}; diff --git a/plugins/import-component/src/components/ImportComponentPage/ImportFinished.tsx b/plugins/import-component/src/components/ImportComponentPage/ImportFinished.tsx new file mode 100644 index 0000000000..6abe551f96 --- /dev/null +++ b/plugins/import-component/src/components/ImportComponentPage/ImportFinished.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import { + Grid, + Link, + Typography, + createStyles, + makeStyles, + Theme, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '100%', + }, + heading: { + fontSize: theme.typography.pxToRem(15), + fontWeight: theme.typography.fontWeightRegular, + }, + linkList: { + display: 'flex', + flexDirection: 'column', + }, + }), +); +type Props = { + nextStep: (options?: { reset: boolean }) => void; + PRLink: string; +}; + +export const ImportFinished: React.FC = ({ nextStep, PRLink }) => { + const classes = useStyles(); + return ( + + + + Pull requests have been successfully opened. You can start again to + import more repositories + + + + Pull request link: + {PRLink} + + + + + + ); +}; diff --git a/plugins/import-component/src/components/ImportComponentPage/ImportStepper.tsx b/plugins/import-component/src/components/ImportComponentPage/ImportStepper.tsx new file mode 100644 index 0000000000..03752fa7d4 --- /dev/null +++ b/plugins/import-component/src/components/ImportComponentPage/ImportStepper.tsx @@ -0,0 +1,42 @@ +/* + * 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 Stepper from '@material-ui/core/Stepper'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import { StepContent } from '@material-ui/core'; + +type Props = { + steps: { step: string; content: React.ReactNode }[]; + activeStep: number; + nextStep: (options?: { reset: boolean }) => void; +}; + +export default function ImportStepper({ steps, activeStep }: Props) { + return ( + + {steps.map(({ step }) => { + const stepProps: { completed?: boolean } = {}; + return ( + + {step} + {steps[activeStep].content} + + ); + })} + + ); +} diff --git a/plugins/import-component/src/components/ImportComponentPage/index.ts b/plugins/import-component/src/components/ImportComponentPage/index.ts new file mode 100644 index 0000000000..dee2816011 --- /dev/null +++ b/plugins/import-component/src/components/ImportComponentPage/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 * from './ImportComponentPage'; diff --git a/plugins/import-component/src/components/ImportComponentPage/useGithubRepos.ts b/plugins/import-component/src/components/ImportComponentPage/useGithubRepos.ts new file mode 100644 index 0000000000..f28d5a444e --- /dev/null +++ b/plugins/import-component/src/components/ImportComponentPage/useGithubRepos.ts @@ -0,0 +1,74 @@ +/* + * 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. + */ +/* + * Copyright 2020 Roadie + * + * 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 * as YAML from 'yaml'; +import { useApi, githubAuthApiRef } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { catalogImportApiRef } from '../../api/CatalogImportApi'; +import { RecursivePartial } from '../../util/types'; + +export function useGithubRepos() { + const api = useApi(catalogImportApiRef); + const auth = useApi(githubAuthApiRef); + + const submitPRToRepo = async (selectedRepo: { + repo: string; + config: RecursivePartial[]; + }) => { + const token = await auth.getAccessToken(['repo']); + + const [ownerName, repoName] = [ + selectedRepo.repo.split('/')[0], + selectedRepo.repo.split('/')[1], + ]; + const submitPRResponse = await api.submitPRToRepo({ + token, + owner: ownerName, + repo: repoName, + fileContent: selectedRepo.config + .map(entity => `---\n${YAML.stringify(entity)}`) + .join('\n'), + }); + + await api.createRepositoryLocation({ + token, + owner: selectedRepo.repo.split('/')[0], + repo: selectedRepo.repo.split('/')[1], + }); + + return submitPRResponse; + }; + + return { + submitPRToRepo, + generateEntityDefinitions: (repo: string) => + api.generateEntityDefinitions({ repo }), + }; +} diff --git a/plugins/import-component/src/components/Router.tsx b/plugins/import-component/src/components/Router.tsx new file mode 100644 index 0000000000..c877d3a208 --- /dev/null +++ b/plugins/import-component/src/components/Router.tsx @@ -0,0 +1,26 @@ +/* + * 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 { Route, Routes } from 'react-router'; +import { ImportComponentPage } from './ImportComponentPage'; + +// As we don't know which path the catalog's router mounted on +// We need to inject this from the app +export const Router = () => ( + + } /> + +); diff --git a/plugins/import-component/src/index.ts b/plugins/import-component/src/index.ts new file mode 100644 index 0000000000..ff7857cacd --- /dev/null +++ b/plugins/import-component/src/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 { plugin } from './plugin'; +export { Router } from './components/Router'; diff --git a/plugins/import-component/src/plugin.test.ts b/plugins/import-component/src/plugin.test.ts new file mode 100644 index 0000000000..dd10754a87 --- /dev/null +++ b/plugins/import-component/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { plugin } from './plugin'; + +describe('import-component', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/import-component/src/plugin.ts b/plugins/import-component/src/plugin.ts new file mode 100644 index 0000000000..a5db8337da --- /dev/null +++ b/plugins/import-component/src/plugin.ts @@ -0,0 +1,54 @@ +/* + * 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. + */ +/* + * Copyright 2020 Roadie 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 { + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, +} from '@backstage/core'; +import { catalogImportApiRef } from './api/CatalogImportApi'; +import { CatalogImportClient } from './api/CatalogImportClient'; + +export const rootRouteRef = createRouteRef({ + path: '', + title: 'import-component', +}); + +export const plugin = createPlugin({ + id: 'import-component', + apis: [ + createApiFactory({ + api: catalogImportApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new CatalogImportClient({ discoveryApi }), + }), + ], +}); diff --git a/plugins/import-component/src/setupTests.ts b/plugins/import-component/src/setupTests.ts new file mode 100644 index 0000000000..825bcd4115 --- /dev/null +++ b/plugins/import-component/src/setupTests.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. + */ + +import '@testing-library/jest-dom'; diff --git a/plugins/import-component/src/util/types.ts b/plugins/import-component/src/util/types.ts new file mode 100644 index 0000000000..77b96f5036 --- /dev/null +++ b/plugins/import-component/src/util/types.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. + */ +export type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object + ? RecursivePartial + : T[P]; +}; diff --git a/plugins/import-component/src/util/validate.test.ts b/plugins/import-component/src/util/validate.test.ts new file mode 100644 index 0000000000..139c7fb0f8 --- /dev/null +++ b/plugins/import-component/src/util/validate.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { ComponentIdValidators } from './validate'; + +describe('ComponentIdValidators', () => { + describe('httpsValidator validator', () => { + const errorMessage = 'Must start with https://.'; + test.each([ + [true, 'https://example.com'], + [errorMessage, 'http://example.com'], + [errorMessage, 'example.com'], + [errorMessage, 'www.example.com'], + [errorMessage, ''], + [errorMessage, undefined], + ])('should return %p for %s', (expected: string | boolean, arg: any) => { + expect(ComponentIdValidators.httpsValidator(arg)).toBe(expected); + }); + }); +}); diff --git a/plugins/import-component/src/util/validate.ts b/plugins/import-component/src/util/validate.ts new file mode 100644 index 0000000000..056131aaae --- /dev/null +++ b/plugins/import-component/src/util/validate.ts @@ -0,0 +1,21 @@ +/* + * 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 const ComponentIdValidators = { + httpsValidator: (value: any) => + (typeof value === 'string' && value.match(/^https:\/\//) !== null) || + 'Must start with https://.', +}; diff --git a/plugins/import-component/tsconfig.json b/plugins/import-component/tsconfig.json new file mode 100644 index 0000000000..b663b01fa2 --- /dev/null +++ b/plugins/import-component/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "dev"], + "compilerOptions": {} +}