From 3435ae9a134ed2184a162ddd143babc8c7c09013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Adaszy=C5=84ski?= Date: Wed, 13 May 2020 08:11:06 +0200 Subject: [PATCH] Add Register Component plugin (#826) Co-authored-by: Wojciech Adaszynski --- packages/app/package.json | 1 + packages/app/src/plugins.ts | 1 + plugins/register-component/.eslintrc.js | 3 + plugins/register-component/README.md | 5 + plugins/register-component/dev/index.tsx | 22 ++++ plugins/register-component/package.json | 41 +++++++ .../RegisterComponentForm.test.tsx | 60 ++++++++++ .../RegisterComponentForm.tsx | 110 ++++++++++++++++++ .../components/RegisterComponentForm/index.ts | 17 +++ .../RegisterComponentPage.test.tsx | 34 ++++++ .../RegisterComponentPage.tsx | 65 +++++++++++ .../components/RegisterComponentPage/index.ts | 17 +++ plugins/register-component/src/index.ts | 17 +++ plugins/register-component/src/plugin.test.ts | 23 ++++ plugins/register-component/src/plugin.ts | 25 ++++ plugins/register-component/src/setupTests.ts | 18 +++ .../src/util/validate.test.ts | 60 ++++++++++ .../register-component/src/util/validate.ts | 27 +++++ plugins/register-component/tsconfig.json | 5 + yarn.lock | 5 + 20 files changed, 556 insertions(+) create mode 100644 plugins/register-component/.eslintrc.js create mode 100644 plugins/register-component/README.md create mode 100644 plugins/register-component/dev/index.tsx create mode 100644 plugins/register-component/package.json create mode 100644 plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx create mode 100644 plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx create mode 100644 plugins/register-component/src/components/RegisterComponentForm/index.ts create mode 100644 plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx create mode 100644 plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx create mode 100644 plugins/register-component/src/components/RegisterComponentPage/index.ts create mode 100644 plugins/register-component/src/index.ts create mode 100644 plugins/register-component/src/plugin.test.ts create mode 100644 plugins/register-component/src/plugin.ts create mode 100644 plugins/register-component/src/setupTests.ts create mode 100644 plugins/register-component/src/util/validate.test.ts create mode 100644 plugins/register-component/src/util/validate.ts create mode 100644 plugins/register-component/tsconfig.json 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 ( +
+ + + + {errors.componentIdInput && ( + + {errors.componentIdInput.message} + + )} + + +
+ ); +}; + +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"