diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index ec26ce5b28..c8ae0382bd 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -20,9 +20,22 @@ import CatalogPage from './CatalogPage'; import { ApiRegistry, ApiProvider, errorApiRef } from '@backstage/core'; import { wrapInTestApp } from '@backstage/test-utils'; import { catalogApiRef } from '../..'; +import { CatalogApi } from '../../api/types'; +import { Entity } from '@backstage/catalog-model'; const errorApi = { post: () => {} }; -const catalogApi = { getEntities: () => Promise.resolve([{ kind: '' }]) }; +const catalogApi: Partial = { + getEntities: () => + Promise.resolve([ + { + metadata: { + name: 'Entity1', + }, + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + }, + ] as Entity[]), +}; describe('CatalogPage', () => { // this test right now causes some red lines in the log output when running tests diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index c2d8408f1f..83be6fb7db 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@backstage/core": "^0.1.1-alpha.6", + "@backstage/catalog-model": "^0.1.1-alpha.6", "@backstage/plugin-catalog": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index 3b8b7f9dcf..e8b0c3b76e 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -15,16 +15,29 @@ */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import RegisterComponentForm from './RegisterComponentForm'; +import { render, fireEvent, cleanup } from '@testing-library/react'; +import RegisterComponentForm, { Props } from './RegisterComponentForm'; +import { act } from 'react-dom/test-utils'; -// TODO(ishmidt): rewrite tests +const setup = (props?: Partial) => { + return { + rendered: render( + , + ), + }; +}; describe('RegisterComponentForm', () => { + afterEach(() => cleanup()); + it('should initially render a disabled button', async () => { - const rendered = render(); + const { rendered } = setup(); 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.', + 'Enter the full path to the service-info.yaml file in GitHub to start tracking your component. It must be in a public repo.', ), ).toBeInTheDocument(); @@ -33,23 +46,21 @@ describe('RegisterComponentForm', () => { }); it('should enable a submit form when data when component url is set ', async () => { - const rendered = render(); + const { rendered } = setup(); const input = (await rendered.getByRole('textbox')) as HTMLInputElement; - fireEvent.change(input, { - target: { value: 'https://example.com/blob/master/service.yaml' }, + await act(async () => { + // react-hook-form uses `input` event for changes + fireEvent.input(input, { + target: { value: 'https://example.com/blob/master/service.yaml' }, + }); }); - const submit = (await rendered.findByText('Submit')) as HTMLButtonElement; + const submit = (await rendered.getByRole('button')) 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(); - }); +}); + +it('should show spinner while submitting', async () => { + const { rendered } = setup({ submitting: true }); + expect(rendered.getByTestId('loading-progress')).toBeInTheDocument(); }); diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index 9a1597dce3..d9fb88e44b 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -20,6 +20,7 @@ import { FormControl, FormHelperText, TextField, + LinearProgress, } from '@material-ui/core'; import { useForm } from 'react-hook-form'; import { makeStyles } from '@material-ui/core/styles'; @@ -37,11 +38,12 @@ const useStyles = makeStyles(theme => ({ }, })); -type RegisterComponentProps = { +export type Props = { onSubmit: (formData: Record) => Promise; + submitting: boolean; }; -const RegisterComponentForm: FC = ({ onSubmit }) => { +const RegisterComponentForm: FC = ({ onSubmit, submitting }) => { const { register, handleSubmit, errors, formState } = useForm({ mode: 'onChange', }); @@ -49,23 +51,27 @@ const RegisterComponentForm: FC = ({ onSubmit }) => { const hasErrors = !!errors.componentLocation; const dirty = formState?.dirty; - return ( + return submitting ? ( + + ) : (
{} }; +const catalogApi: jest.Mocked = { + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + addLocation: jest.fn((_a, _b) => new Promise(() => {})), + getEntities: jest.fn(), + getEntityByName: jest.fn(), +}; + +const setup = () => ({ + rendered: render( + + + + + + + , + ), +}); describe('RegisterComponentPage', () => { + afterEach(() => cleanup()); + it('should render', () => { - mockFetch.mockResponse(() => new Promise(() => {})); - const rendered = render( - - - , - ); - expect(rendered.getByText('Register Component')).toBeInTheDocument(); + const { rendered } = setup(); + expect( + rendered.getByText('Register existing component'), + ).toBeInTheDocument(); }); }); diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 6d01fc469e..27654d2080 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -15,22 +15,7 @@ */ import React, { FC, useState } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; -import { - Grid, - makeStyles, - DialogTitle, - Dialog, - DialogContent, - DialogContentText, - DialogActions, - Button, - ListItem, - List, - LinearProgress, - Divider, - Link, -} from '@material-ui/core'; +import { Grid, makeStyles } from '@material-ui/core'; import { InfoCard, Page, @@ -38,16 +23,13 @@ import { Content, useApi, errorApiRef, - StructuredMetadataTable, Header, } from '@backstage/core'; import RegisterComponentForm from '../RegisterComponentForm'; -import { - entityRoute, - rootRoute as catalogRootRoute, - catalogApiRef -} from '@backstage/plugin-catalog'; -import { generatePath } from 'react-router'; +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { useMountedState } from 'react-use'; +import { Entity, Location } from '@backstage/catalog-model'; +import { RegisterComponentResultDialog } from '../RegisterComponentResultDialog'; const useStyles = makeStyles(theme => ({ dialogPaper: { @@ -76,11 +58,15 @@ const RegisterComponentPage: FC<{}> = () => { const [formState, setFormState] = useState>( FormStates.Idle, ); + const isMounted = useMountedState(); const errorApi = useApi(errorApiRef); const [result, setResult] = useState<{ - data: any; + data: { + entities: Entity[]; + location: Location; + } | null; error: null | Error; }>({ data: null, @@ -93,12 +79,17 @@ const RegisterComponentPage: FC<{}> = () => { try { const data = await catalogApi.addLocation('github', target); + if (!isMounted()) return; + setResult({ error: null, data }); setFormState(FormStates.Success); } catch (e) { + errorApi.post(e); + + if (!isMounted()) return; + setResult({ error: e, data: null }); setFormState(FormStates.Idle); - errorApi.post(e); } }; @@ -109,70 +100,21 @@ const RegisterComponentPage: FC<{}> = () => { - {formState === FormStates.Submitting ? ( - - ) : ( - - )} + - setFormState(FormStates.Idle)} - classes={{ paper: classes.dialogPaper }} - > - Component registration result - {formState === FormStates.Success && ( - <> - - - Following components have been succefully created: - - {result.data.entities.map((entity: any, index: number) => ( - <> - - - {generatePath(entityRoute.path, { - name: entity.metadata.name, - })} - - ), - }} - /> - - {index < result.data.entities.length - 1 && ( - - )} - - ))} - - - - - - - - )} - + {formState === FormStates.Success && ( + setFormState(FormStates.Idle)} + classes={{ paper: classes.dialogPaper }} + /> + )} ); }; diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.test.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.test.tsx new file mode 100644 index 0000000000..284e83121c --- /dev/null +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.test.tsx @@ -0,0 +1,80 @@ +/* + * 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, { ComponentProps } from 'react'; +import { render, cleanup } from '@testing-library/react'; +import { RegisterComponentResultDialog } from './RegisterComponentResultDialog'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { MemoryRouter } from 'react-router-dom'; +import { Entity } from '@backstage/catalog-model'; + +const setup = ( + props?: Partial>, +) => ({ + rendered: render( + + + {}} + entities={[]} + {...props} + /> + + , + ), +}); +describe('RegisterComponentResultDialog', () => { + afterEach(() => cleanup()); + + it('should render', () => { + const { rendered } = setup(); + expect( + rendered.getByText('Component registration result'), + ).toBeInTheDocument(); + }); +}); + +it('should show a list of components if success', async () => { + const { rendered } = setup({ + entities: [ + { + kind: 'Component', + metadata: { + name: 'Component1', + }, + spec: { + type: 'website', + }, + }, + { + kind: 'Component', + metadata: { + name: 'Component2', + }, + spec: { + type: 'service', + }, + }, + ] as Entity[], + }); + + expect( + rendered.getByText('Following components have been succefully created:'), + ).toBeInTheDocument(); + expect(rendered.getByText('Component1')).toBeInTheDocument(); + expect(rendered.getByText('Component2')).toBeInTheDocument(); +}); diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx new file mode 100644 index 0000000000..ead7323870 --- /dev/null +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -0,0 +1,93 @@ +/* + * 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 { + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + List, + ListItem, + Link, + Divider, + DialogActions, + Button, +} from '@material-ui/core'; +import { Entity } from '@backstage/catalog-model'; +import { StructuredMetadataTable } from '@backstage/core'; +import { generatePath } from 'react-router'; +import { + entityRoute, + rootRoute as catalogRootRoute, +} from '@backstage/plugin-catalog'; +import { Link as RouterLink } from 'react-router-dom'; + +type Props = { + onClose: () => void; + classes?: Record; + entities: Entity[]; +}; + +export const RegisterComponentResultDialog: FC = ({ + onClose, + classes, + entities, +}) => ( + + Component registration result + + + Following components have been succefully created: + + + {entities.map((entity: any, index: number) => ( + + + + {generatePath(entityRoute.path, { + name: entity.metadata.name, + })} + + ), + }} + /> + + {index < entities.length - 1 && } + + ))} + + + + + + +); diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/index.ts b/plugins/register-component/src/components/RegisterComponentResultDialog/index.ts new file mode 100644 index 0000000000..7ed25f3c20 --- /dev/null +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/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 { RegisterComponentResultDialog } from './RegisterComponentResultDialog';