diff --git a/packages/app/package.json b/packages/app/package.json
index baf0e9628c..255f66e5e2 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -9,6 +9,7 @@
"@backstage/plugin-home-page": "^0.1.1-alpha.4",
"@backstage/plugin-catalog": "^0.1.1-alpha.4",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.4",
+ "@backstage/plugin-register-component": "^0.1.1-alpha.4",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.4",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.4",
"@backstage/plugin-welcome": "^0.1.1-alpha.4",
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index c8d972bf8d..1930acae92 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -20,3 +20,4 @@ export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
export { plugin as Explore } from '@backstage/plugin-explore';
+export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
diff --git a/plugins/register-component/.eslintrc.js b/plugins/register-component/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/register-component/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/register-component/README.md b/plugins/register-component/README.md
new file mode 100644
index 0000000000..729a38955c
--- /dev/null
+++ b/plugins/register-component/README.md
@@ -0,0 +1,5 @@
+# [WIP] register-component
+
+Welcome to the register-component plugin!
+
+This plugin allows you to submit your Backstage component using your software's YAML config.
diff --git a/plugins/register-component/dev/index.tsx b/plugins/register-component/dev/index.tsx
new file mode 100644
index 0000000000..d97643057b
--- /dev/null
+++ b/plugins/register-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/register-component/package.json b/plugins/register-component/package.json
new file mode 100644
index 0000000000..9113006ad7
--- /dev/null
+++ b/plugins/register-component/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@backstage/plugin-register-component",
+ "version": "0.1.1-alpha.4",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "scripts": {
+ "build": "backstage-cli plugin:build",
+ "start": "backstage-cli plugin:serve",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "diff": "backstage-cli plugin:diff",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/core": "^0.1.1-alpha.4",
+ "@backstage/theme": "^0.1.1-alpha.4",
+ "@material-ui/core": "^4.9.1",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.45",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "react-hook-form": "^5.7.2",
+ "react-use": "^13.24.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.1.1-alpha.4",
+ "@backstage/dev-utils": "^0.1.1-alpha.4",
+ "@testing-library/jest-dom": "^4.2.4",
+ "@testing-library/react": "^9.3.2",
+ "@testing-library/user-event": "^7.1.2",
+ "@types/jest": "^24.0.0",
+ "@types/node": "^12.0.0",
+ "@types/testing-library__jest-dom": "^5.0.4",
+ "jest-fetch-mock": "^3.0.3"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx
new file mode 100644
index 0000000000..9203093d48
--- /dev/null
+++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 { render, fireEvent } from '@testing-library/react';
+import RegisterComponentForm from './RegisterComponentForm';
+
+describe('RegisterComponentForm', () => {
+ it('should initially render a disabled button', async () => {
+ const rendered = render(
+ ,
+ );
+ expect(
+ await rendered.findByText(
+ 'Enter the full path to the service-info.yaml file in GHE to start tracking your component. It must be in a public repo, on the master branch.',
+ ),
+ ).toBeInTheDocument();
+
+ const submit = (await rendered.getByRole('button')) as HTMLButtonElement;
+ expect(submit.disabled).toBeTruthy();
+ });
+
+ it('should enable a submit form when data when component url is set ', async () => {
+ const rendered = render(
+ ,
+ );
+ const input = (await rendered.getByRole('textbox')) as HTMLInputElement;
+ fireEvent.change(input, {
+ target: { value: 'https://example.com/blob/master/service.yaml' },
+ });
+ const submit = (await rendered.findByText('Submit')) as HTMLButtonElement;
+
+ expect(submit.disabled).toBeFalsy();
+ });
+
+ it('should hide input on submission ', async () => {
+ const rendered = render(
+ ,
+ );
+
+ expect(
+ await rendered.findByText(
+ 'Your component is being registered. Please wait.',
+ ),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx
new file mode 100644
index 0000000000..d279afac25
--- /dev/null
+++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { FC } from 'react';
+import {
+ Button,
+ FormControl,
+ FormHelperText,
+ TextField,
+ Typography,
+} from '@material-ui/core';
+import { useForm } from 'react-hook-form';
+import { makeStyles } from '@material-ui/core/styles';
+import { BackstageTheme } from '@backstage/theme';
+import { Progress } from '@backstage/core';
+import { ComponentIdValidators } from '../../util/validate';
+
+const useStyles = makeStyles(theme => ({
+ form: {
+ alignItems: 'flex-start',
+ display: 'flex',
+ flexFlow: 'column nowrap',
+ },
+ submit: {
+ marginTop: theme.spacing(1),
+ },
+}));
+
+type RegisterComponentProps = {
+ onSubmit: () => any;
+ submitting: boolean;
+};
+
+const RegisterComponentForm: FC = ({
+ onSubmit,
+ submitting,
+}) => {
+ const { register, handleSubmit, errors, formState } = useForm({
+ mode: 'onChange',
+ });
+ const classes = useStyles();
+ const hasErrors = !!errors.componentIdInput;
+ const dirty = formState?.dirty;
+ if (submitting) {
+ return (
+ <>
+
+ Your component is being registered. Please wait.
+
+
+ >
+ );
+ }
+ return (
+
+ );
+};
+
+export default RegisterComponentForm;
diff --git a/plugins/register-component/src/components/RegisterComponentForm/index.ts b/plugins/register-component/src/components/RegisterComponentForm/index.ts
new file mode 100644
index 0000000000..beb0d00ecc
--- /dev/null
+++ b/plugins/register-component/src/components/RegisterComponentForm/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 { default } from './RegisterComponentForm';
diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx
new file mode 100644
index 0000000000..ac2645135e
--- /dev/null
+++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx
@@ -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.
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import mockFetch from 'jest-fetch-mock';
+import RegisterComponentPage from './RegisterComponentPage';
+import { ThemeProvider } from '@material-ui/core';
+import { lightTheme } from '@backstage/theme';
+
+describe('RegisterComponentPage', () => {
+ it('should render', () => {
+ mockFetch.mockResponse(() => new Promise(() => {}));
+ const rendered = render(
+
+
+ ,
+ );
+ expect(rendered.getByText('Register Component')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx
new file mode 100644
index 0000000000..114c37fe4c
--- /dev/null
+++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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, { FC, useEffect, useState } from 'react';
+import { Grid } from '@material-ui/core';
+import {
+ InfoCard,
+ Page,
+ pageTheme,
+ Content,
+ ContentHeader,
+ SupportButton,
+} from '@backstage/core';
+import RegisterComponentForm from '../RegisterComponentForm';
+
+const RegisterComponentPage: FC<{}> = () => {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (isSubmitting) {
+ setTimeout(() => {
+ setIsSubmitting(false);
+ }, 4000);
+ }
+ }, [isSubmitting]);
+
+ const onSubmit = () => {
+ setIsSubmitting(true);
+ };
+
+ return (
+
+
+
+ Documentation
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default RegisterComponentPage;
diff --git a/plugins/register-component/src/components/RegisterComponentPage/index.ts b/plugins/register-component/src/components/RegisterComponentPage/index.ts
new file mode 100644
index 0000000000..e0757e0eee
--- /dev/null
+++ b/plugins/register-component/src/components/RegisterComponentPage/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 { default } from './RegisterComponentPage';
diff --git a/plugins/register-component/src/index.ts b/plugins/register-component/src/index.ts
new file mode 100644
index 0000000000..3a0a0fe2d3
--- /dev/null
+++ b/plugins/register-component/src/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 { plugin } from './plugin';
diff --git a/plugins/register-component/src/plugin.test.ts b/plugins/register-component/src/plugin.test.ts
new file mode 100644
index 0000000000..1e61060202
--- /dev/null
+++ b/plugins/register-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('register-component', () => {
+ it('should export plugin', () => {
+ expect(plugin).toBeDefined();
+ });
+});
diff --git a/plugins/register-component/src/plugin.ts b/plugins/register-component/src/plugin.ts
new file mode 100644
index 0000000000..f32e9dc84f
--- /dev/null
+++ b/plugins/register-component/src/plugin.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { createPlugin } from '@backstage/core';
+import RegisterComponentPage from './components/RegisterComponentPage';
+
+export const plugin = createPlugin({
+ id: 'register-component',
+ register({ router }) {
+ router.registerRoute('/register-component', RegisterComponentPage);
+ },
+});
diff --git a/plugins/register-component/src/setupTests.ts b/plugins/register-component/src/setupTests.ts
new file mode 100644
index 0000000000..1a907ab8e6
--- /dev/null
+++ b/plugins/register-component/src/setupTests.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.
+ */
+
+import '@testing-library/jest-dom/extend-expect';
+require('jest-fetch-mock').enableMocks();
diff --git a/plugins/register-component/src/util/validate.test.ts b/plugins/register-component/src/util/validate.test.ts
new file mode 100644
index 0000000000..43e05a4671
--- /dev/null
+++ b/plugins/register-component/src/util/validate.test.ts
@@ -0,0 +1,60 @@
+/*
+ * 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);
+ });
+ });
+ describe('masterValidator', () => {
+ const errorMessage = 'Must reference a file on the master branch.';
+ test.each([
+ [true, '/blob/master/'],
+ [true, 'http://example.com/blob/master/'],
+ [errorMessage, 'blob/master/'],
+ [errorMessage, '/blob/master'],
+ [errorMessage, '/master/'],
+ [errorMessage, ''],
+ [errorMessage, undefined],
+ ])('should return %p for %s', (expected: string | boolean, arg: any) => {
+ expect(ComponentIdValidators.masterValidator(arg)).toBe(expected);
+ });
+ });
+ describe('yamlValidator', () => {
+ const errorMessage = "Must end with '.yaml'.";
+ test.each([
+ [true, '.yaml'],
+ [true, 'http://example.com/blob/master/service.yaml'],
+ [true, 'https://example.yaml'],
+ [errorMessage, '.yml'],
+ [errorMessage, 'http://example.com/blob/master/service'],
+ [errorMessage, undefined],
+ ])('should return %p for %s', (expected: string | boolean, arg: any) => {
+ expect(ComponentIdValidators.yamlValidator(arg)).toBe(expected);
+ });
+ });
+});
diff --git a/plugins/register-component/src/util/validate.ts b/plugins/register-component/src/util/validate.ts
new file mode 100644
index 0000000000..175d9378df
--- /dev/null
+++ b/plugins/register-component/src/util/validate.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.
+ */
+
+export const ComponentIdValidators = {
+ httpsValidator: (value: any) =>
+ (typeof value === 'string' && value.match(/^https:\/\//) !== null) ||
+ 'Must start with https://.',
+ masterValidator: (value: any) =>
+ (typeof value === 'string' && value.match(/\/blob\/master\//) !== null) ||
+ 'Must reference a file on the master branch.',
+ yamlValidator: (value: any) =>
+ (typeof value === 'string' && value.match(/.yaml$/) !== null) ||
+ "Must end with '.yaml'.",
+};
diff --git a/plugins/register-component/tsconfig.json b/plugins/register-component/tsconfig.json
new file mode 100644
index 0000000000..b663b01fa2
--- /dev/null
+++ b/plugins/register-component/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": ["src", "dev"],
+ "compilerOptions": {}
+}
diff --git a/yarn.lock b/yarn.lock
index 6b716af0dd..9f974135a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17772,6 +17772,11 @@ react-helmet@5.2.1:
react-fast-compare "^2.0.2"
react-side-effect "^1.1.0"
+react-hook-form@^5.7.2:
+ version "5.7.2"
+ resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-5.7.2.tgz#a84e259e5d37dd30949af4f79c4dac31101b79ac"
+ integrity sha512-bJvY348vayIvEUmSK7Fvea/NgqbT2racA2IbnJz/aPlQ3GBtaTeDITH6rtCa6y++obZzG6E3Q8VuoXPir7QYUg==
+
react-hotkeys@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz#a7719c7340cbba888b0e9184f806a9ec0ac2c53f"