Add Register Component plugin (#826)

Co-authored-by: Wojciech Adaszynski <wojciecha@spotify.com>
This commit is contained in:
Wojciech Adaszyński
2020-05-13 08:11:06 +02:00
committed by GitHub
parent e399a3f8d0
commit 3435ae9a13
20 changed files with 556 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+5
View File
@@ -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.
+22
View File
@@ -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();
+41
View File
@@ -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"
]
}
@@ -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(
<RegisterComponentForm onSubmit={jest.fn()} submitting={false} />,
);
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(
<RegisterComponentForm onSubmit={jest.fn()} submitting={false} />,
);
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(
<RegisterComponentForm onSubmit={jest.fn()} submitting />,
);
expect(
await rendered.findByText(
'Your component is being registered. Please wait.',
),
).toBeInTheDocument();
});
});
@@ -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<BackstageTheme>(theme => ({
form: {
alignItems: 'flex-start',
display: 'flex',
flexFlow: 'column nowrap',
},
submit: {
marginTop: theme.spacing(1),
},
}));
type RegisterComponentProps = {
onSubmit: () => any;
submitting: boolean;
};
const RegisterComponentForm: FC<RegisterComponentProps> = ({
onSubmit,
submitting,
}) => {
const { register, handleSubmit, errors, formState } = useForm({
mode: 'onChange',
});
const classes = useStyles();
const hasErrors = !!errors.componentIdInput;
const dirty = formState?.dirty;
if (submitting) {
return (
<>
<Typography variant="subtitle1" paragraph>
Your component is being registered. Please wait.
</Typography>
<Progress />
</>
);
}
return (
<form
autoComplete="off"
onSubmit={handleSubmit(onSubmit)}
className={classes.form}
>
<FormControl>
<TextField
id="registerComponentInput"
variant="outlined"
label="Component service file URL"
error={hasErrors}
placeholder="https://example.com/user/some-service/blob/master/service-info.yaml"
name="componentIdInput"
required
margin="normal"
helperText="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."
inputRef={register({
required: true,
validate: ComponentIdValidators,
})}
/>
{errors.componentIdInput && (
<FormHelperText error={hasErrors} id="register-component-helper-text">
{errors.componentIdInput.message}
</FormHelperText>
)}
</FormControl>
<Button
id="registerComponentFormSubmit"
variant="contained"
color="primary"
type="submit"
disabled={!dirty || hasErrors}
className={classes.submit}
>
Submit
</Button>
</form>
);
};
export default RegisterComponentForm;
@@ -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';
@@ -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(
<ThemeProvider theme={lightTheme}>
<RegisterComponentPage />
</ThemeProvider>,
);
expect(rendered.getByText('Register Component')).toBeInTheDocument();
});
});
@@ -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 (
<Page theme={pageTheme.tool}>
<Content>
<ContentHeader title="Register Component">
<SupportButton>Documentation</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title="Start tracking your component in Backstage">
<RegisterComponentForm
onSubmit={onSubmit}
submitting={isSubmitting}
/>
</InfoCard>
</Grid>
</Grid>
</Content>
</Page>
);
};
export default RegisterComponentPage;
@@ -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';
+17
View File
@@ -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';
@@ -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();
});
});
+25
View File
@@ -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);
},
});
@@ -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();
@@ -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);
});
});
});
@@ -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'.",
};
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}