From cef8abb5c4ba652c73bdb1e2545b13af9967ac2d Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Fri, 29 May 2020 14:48:12 +0200 Subject: [PATCH 01/14] add register button, addLocation api Co-authored-by: Nikita Dudnik Co-authored-by: Ben Lambert --- packages/app/package.json | 4 ++-- packages/backend/src/index.ts | 12 ++++++------ plugins/catalog/src/api/CatalogClient.ts | 17 +++++++++++++++++ .../src/components/ScaffolderPage/index.tsx | 14 ++++++++++++-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index ed2f547ad7..0af25d6f3a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -73,10 +73,10 @@ } }, "/catalog/api": { - "target": "http://localhost:3003", + "target": "http://localhost:7000", "changeOrigin": true, "pathRewrite": { - "^/catalog/api/": "/" + "^/catalog/api/": "/catalog/" } } } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 5b1fc54330..a6299ca829 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -70,12 +70,12 @@ async function main() { app.use(requestLoggingHandler()); app.use('/catalog', await catalog(createEnv('catalog'))); app.use('/scaffolder', await scaffolder(createEnv('scaffolder'))); - app.use( - '/sentry', - await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })), - ); - app.use('/auth', await auth(createEnv('auth'))); - app.use('/identity', await identity(createEnv('identity'))); + // app.use( + // '/sentry', + // await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })), + // ); + // app.use('/auth', await auth(createEnv('auth'))); + // app.use('/identity', await identity(createEnv('identity'))); app.use(notFoundHandler()); app.use(errorHandler()); diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 42ef1c4da5..7449ce2f2c 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -16,6 +16,7 @@ import { CatalogApi } from './types'; import { DescriptorEnvelope } from '../types'; +import {} from '@backstage/catalog-model'; export class CatalogClient implements CatalogApi { private apiOrigin: string; @@ -42,4 +43,20 @@ export class CatalogClient implements CatalogApi { if (entity) return entity; throw new Error(`'Entity not found: ${name}`); } + + async addLocation(type: string, target: string): Promise<{}> { + const response = await fetch( + `${this.apiOrigin}${this.basePath}/locations`, + { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ type, target }), + }, + ); + const location = await response.json(); + if (location) return location; + throw new Error(`'Location wasn't added: ${target}`); + } } diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index e6fed50cef..59eb6cff43 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -24,7 +24,8 @@ import { Page, pageTheme, } from '@backstage/core'; -import { Typography, Link } from '@material-ui/core'; +import { Typography, Link, Button } from '@material-ui/core'; +import { Link as RouterLink } from 'react-router-dom'; // TODO(blam): Connect to backend const STATIC_DATA = [ @@ -49,7 +50,16 @@ const ScaffolderPage: React.FC<{}> = () => { subtitle="Create new software components using standard templates" /> - + + + NOTE! This feature is WIP. You can follow progress{' '} From 304c544af51a746b52391a3bdf94da3098620177 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Fri, 29 May 2020 14:59:59 +0200 Subject: [PATCH 02/14] Posting addLocation --- plugins/catalog/src/api/CatalogClient.ts | 2 +- .../RegisterComponentPage.tsx | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 7449ce2f2c..94ee9aa4b3 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -16,7 +16,6 @@ import { CatalogApi } from './types'; import { DescriptorEnvelope } from '../types'; -import {} from '@backstage/catalog-model'; export class CatalogClient implements CatalogApi { private apiOrigin: string; @@ -44,6 +43,7 @@ export class CatalogClient implements CatalogApi { throw new Error(`'Entity not found: ${name}`); } + // TODO: Types for the type param async addLocation(type: string, target: string): Promise<{}> { const response = await fetch( `${this.apiOrigin}${this.basePath}/locations`, diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 114c37fe4c..33029a012d 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -23,22 +23,23 @@ import { Content, ContentHeader, SupportButton, + useApi, } from '@backstage/core'; import RegisterComponentForm from '../RegisterComponentForm'; +import { catalogApiRef } from '@backstage/plugin-catalog'; const RegisterComponentPage: FC<{}> = () => { + const catalogApi = useApi(catalogApiRef); const [isSubmitting, setIsSubmitting] = useState(false); - useEffect(() => { - if (isSubmitting) { - setTimeout(() => { - setIsSubmitting(false); - }, 4000); - } - }, [isSubmitting]); - - const onSubmit = () => { + const onSubmit = async (formData: { componentIdInput: string }) => { setIsSubmitting(true); + + const { componentIdInput: target } = formData; + + const location = await catalogApi.addLocation('github', target); + + setIsSubmitting(false); }; return ( From 99e8009dadf1eabacd79a0abd970f08547b72c9e Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Fri, 29 May 2020 16:51:25 +0200 Subject: [PATCH 03/14] feat: success and error from the api Co-authored-by: Nikita Dudnik Co-authored-by: Ben Lambert --- plugins/catalog/src/api/CatalogClient.ts | 3 +++ plugins/catalog/src/api/types.ts | 1 + .../RegisterComponentPage.tsx | 18 +++++++++++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 94ee9aa4b3..1b3b26a11e 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -55,6 +55,9 @@ export class CatalogClient implements CatalogApi { body: JSON.stringify({ type, target }), }, ); + if (response.status !== 201) { + throw new Error(`Location wasn't added: ${target}`); + } const location = await response.json(); if (location) return location; throw new Error(`'Location wasn't added: ${target}`); diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index eb2cdca562..a016842e41 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -25,4 +25,5 @@ export const catalogApiRef = createApiRef({ export interface CatalogApi { getEntities(): Promise; getEntityByName(name: string): Promise; + addLocation(type: string, target: string): Promise<{}>; } diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 33029a012d..b44e3aa238 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC, useEffect, useState } from 'react'; +import React, { FC, useState } from 'react'; import { Grid } from '@material-ui/core'; import { InfoCard, @@ -24,6 +24,8 @@ import { ContentHeader, SupportButton, useApi, + alertApiRef, + errorApiRef, } from '@backstage/core'; import RegisterComponentForm from '../RegisterComponentForm'; import { catalogApiRef } from '@backstage/plugin-catalog'; @@ -32,12 +34,22 @@ const RegisterComponentPage: FC<{}> = () => { const catalogApi = useApi(catalogApiRef); const [isSubmitting, setIsSubmitting] = useState(false); + const alertApi = useApi(alertApiRef); + const errorApi = useApi(errorApiRef); + const onSubmit = async (formData: { componentIdInput: string }) => { setIsSubmitting(true); const { componentIdInput: target } = formData; - - const location = await catalogApi.addLocation('github', target); + try { + await catalogApi.addLocation('github', target); + alertApi.post({ + message: 'Successfully added the location', + severity: 'success', + }); + } catch (e) { + errorApi.post(e); + } setIsSubmitting(false); }; From a8643dc8dbe26cc3a31205cbe75fd65a31b0dfee Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 1 Jun 2020 17:14:38 +0200 Subject: [PATCH 04/14] Feedback for the entity registration Co-authored-by: Nikita Dudnik --- plugins/catalog/src/api/CatalogClient.ts | 43 +++++++- plugins/catalog/src/api/types.ts | 5 +- .../RegisterComponentPage.tsx | 97 +++++++++++++++++-- 3 files changed, 135 insertions(+), 10 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 1b3b26a11e..ed38c008ac 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -44,7 +44,7 @@ export class CatalogClient implements CatalogApi { } // TODO: Types for the type param - async addLocation(type: string, target: string): Promise<{}> { + async addLocation(type: string, target: string) { const response = await fetch( `${this.apiOrigin}${this.basePath}/locations`, { @@ -59,7 +59,46 @@ export class CatalogClient implements CatalogApi { throw new Error(`Location wasn't added: ${target}`); } const location = await response.json(); - if (location) return location; + + // TODO(shmidt-i): remove mocks + if (location) + return { + location, + entities: [ + { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'component-website', + annotations: { + 'backstage.io/managed-by-location': location.id, + }, + uid: 'uid1', + etag: 'YTQzMmNmNjctMGZmMC00YWM5LWFjYWQtZDg1NjBjNDFlYWM4', + generation: 1, + }, + spec: { + type: 'website', + }, + }, + { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'component-service', + annotations: { + 'backstage.io/managed-by-location': location.id, + }, + uid: 'uid2', + etag: 'YTQzMmNmNjctMGZmMC00YWM5LWFjYWQtZDg1NjBjNDFlYWM4', + generation: 1, + }, + spec: { + type: 'service', + }, + }, + ], + }; throw new Error(`'Location wasn't added: ${target}`); } } diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index a016842e41..b92f07247f 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -25,5 +25,8 @@ export const catalogApiRef = createApiRef({ export interface CatalogApi { getEntities(): Promise; getEntityByName(name: string): Promise; - addLocation(type: string, target: string): Promise<{}>; + addLocation( + type: string, + target: string, + ): Promise<{ location: any; entities: Entity[] }>; } diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index b44e3aa238..de830f9a43 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -15,7 +15,24 @@ */ import React, { FC, useState } from 'react'; -import { Grid } from '@material-ui/core'; +import { useHistory, Link as RouterLink } from 'react-router-dom'; +import { + Grid, + makeStyles, + DialogTitle, + Dialog, + DialogContent, + DialogContentText, + DialogActions, + Button, + ListItem, + ListItemText, + List, + LinearProgress, +} from '@material-ui/core'; +import { GitHub as GitHubIcon } from '@material-ui/icons'; +import { Star as StarIcon } from '@material-ui/icons'; + import { InfoCard, Page, @@ -30,30 +47,60 @@ import { import RegisterComponentForm from '../RegisterComponentForm'; import { catalogApiRef } from '@backstage/plugin-catalog'; +const useStyles = makeStyles(theme => ({ + dialogPaper: { + minHeight: 250, + minWidth: 600, + }, + icon: { + width: 20, + marginRight: theme.spacing(1), + }, + contentText: { + paddingBottom: theme.spacing(2), + }, +})); + const RegisterComponentPage: FC<{}> = () => { + const history = useHistory(); + const classes = useStyles(); + const catalogApi = useApi(catalogApiRef); const [isSubmitting, setIsSubmitting] = useState(false); const alertApi = useApi(alertApiRef); const errorApi = useApi(errorApiRef); + const [result, setResult] = useState<{ + data: any; + error: null | Error; + loading: boolean; + }>({ + data: null, + error: null, + loading: false, + }); + const onSubmit = async (formData: { componentIdInput: string }) => { setIsSubmitting(true); const { componentIdInput: target } = formData; try { - await catalogApi.addLocation('github', target); + const data = await catalogApi.addLocation('github', target); + alertApi.post({ message: 'Successfully added the location', severity: 'success', }); + setResult({ error: null, loading: false, data }); } catch (e) { errorApi.post(e); } setIsSubmitting(false); }; - + const gheUrl = 'some-url'; + const showDialog = result.data && !result.error; return ( @@ -63,14 +110,50 @@ const RegisterComponentPage: FC<{}> = () => { - + {result.loading ? ( + + ) : ( + + )} + + Component registration result + {result.data ? ( + <> + + + Following components have been succefully created. + + {result.data.entities.map((entity: any) => ( + + + + + + ))} + + + + + + + + ) : ( + + + Your component is being created. Please wait. + + + )} + ); }; From fcb6c70587b883f60821b8c6ad8be82945c00c8f Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 2 Jun 2020 11:05:58 +0200 Subject: [PATCH 05/14] Enable hot-reload --- packages/app/src/App.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c18169ae03..31b68a9da5 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -20,6 +20,7 @@ import { BrowserRouter as Router } from 'react-router-dom'; import Root from './components/Root'; import * as plugins from './plugins'; import apis from './apis'; +import { hot } from 'react-hot-loader/root'; const app = createApp({ apis, @@ -53,4 +54,4 @@ const App: FC<{}> = () => ( ); -export default App; +export default hot(App); From ef3ed1a54a809f305b2d150d00b03bf9b83302eb Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 2 Jun 2020 11:06:12 +0200 Subject: [PATCH 06/14] Cleanup for the register component flow --- .../RegisterComponentForm.tsx | 29 +++------ .../RegisterComponentPage.tsx | 63 +++++++++++-------- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index d279afac25..9a1597dce3 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -20,12 +20,10 @@ import { 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 => ({ @@ -40,30 +38,17 @@ const useStyles = makeStyles(theme => ({ })); type RegisterComponentProps = { - onSubmit: () => any; - submitting: boolean; + onSubmit: (formData: Record) => Promise; }; -const RegisterComponentForm: FC = ({ - onSubmit, - submitting, -}) => { +const RegisterComponentForm: FC = ({ onSubmit }) => { const { register, handleSubmit, errors, formState } = useForm({ mode: 'onChange', }); const classes = useStyles(); - const hasErrors = !!errors.componentIdInput; + const hasErrors = !!errors.componentLocation; const dirty = formState?.dirty; - if (submitting) { - return ( - <> - - Your component is being registered. Please wait. - - - - ); - } + return (
= ({ label="Component service file URL" error={hasErrors} placeholder="https://example.com/user/some-service/blob/master/service-info.yaml" - name="componentIdInput" + name="componentLocation" 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." @@ -87,9 +72,9 @@ const RegisterComponentForm: FC = ({ })} /> - {errors.componentIdInput && ( + {errors.componentLocation && ( - {errors.componentIdInput.message} + {errors.componentLocation.message} )} diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index de830f9a43..c67dc158c8 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -15,7 +15,7 @@ */ import React, { FC, useState } from 'react'; -import { useHistory, Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink } from 'react-router-dom'; import { Grid, makeStyles, @@ -29,10 +29,9 @@ import { ListItemText, List, LinearProgress, + ListItemIcon, } from '@material-ui/core'; -import { GitHub as GitHubIcon } from '@material-ui/icons'; -import { Star as StarIcon } from '@material-ui/icons'; - +import LinkIcon from '@material-ui/icons/Link'; import { InfoCard, Page, @@ -61,12 +60,20 @@ const useStyles = makeStyles(theme => ({ }, })); -const RegisterComponentPage: FC<{}> = () => { - const history = useHistory(); - const classes = useStyles(); +const FormStates = { + Idle: 'idle', + Success: 'success', + Error: 'error', + Submitting: 'submitting', +} as const; +type ValuesOf = T extends Record ? V : never; +const RegisterComponentPage: FC<{}> = () => { + const classes = useStyles(); const catalogApi = useApi(catalogApiRef); - const [isSubmitting, setIsSubmitting] = useState(false); + const [formState, setFormState] = useState>( + FormStates.Idle, + ); const alertApi = useApi(alertApiRef); const errorApi = useApi(errorApiRef); @@ -81,10 +88,9 @@ const RegisterComponentPage: FC<{}> = () => { loading: false, }); - const onSubmit = async (formData: { componentIdInput: string }) => { - setIsSubmitting(true); - - const { componentIdInput: target } = formData; + const handleSubmit = async (formData: Record) => { + setFormState(FormStates.Submitting); + const { componentLocation: target } = formData; try { const data = await catalogApi.addLocation('github', target); @@ -93,45 +99,50 @@ const RegisterComponentPage: FC<{}> = () => { severity: 'success', }); setResult({ error: null, loading: false, data }); + setFormState(FormStates.Success); } catch (e) { + setFormState(FormStates.Error); errorApi.post(e); } - - setIsSubmitting(false); }; - const gheUrl = 'some-url'; - const showDialog = result.data && !result.error; + return ( - + Documentation - {result.loading ? ( + {formState === FormStates.Submitting ? ( ) : ( - + )} - + setFormState(FormStates.Idle)} + classes={{ paper: classes.dialogPaper }} + > Component registration result {result.data ? ( <> - Following components have been succefully created. + Following components have been succefully created: {result.data.entities.map((entity: any) => ( + + + @@ -141,8 +152,8 @@ const RegisterComponentPage: FC<{}> = () => { - From 6060face132fb955c4c97c8155bcb4afc669ff67 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 2 Jun 2020 11:38:38 +0200 Subject: [PATCH 07/14] Wire up real data --- plugins/catalog/src/api/CatalogClient.ts | 40 +----------- plugins/catalog/src/api/types.ts | 4 +- .../RegisterComponentPage.tsx | 64 +++++++++---------- 3 files changed, 36 insertions(+), 72 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index ed38c008ac..6784c08494 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -58,46 +58,12 @@ export class CatalogClient implements CatalogApi { if (response.status !== 201) { throw new Error(`Location wasn't added: ${target}`); } - const location = await response.json(); + const { location, entities } = await response.json(); - // TODO(shmidt-i): remove mocks - if (location) + if (location && entities.length > 0) return { location, - entities: [ - { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { - name: 'component-website', - annotations: { - 'backstage.io/managed-by-location': location.id, - }, - uid: 'uid1', - etag: 'YTQzMmNmNjctMGZmMC00YWM5LWFjYWQtZDg1NjBjNDFlYWM4', - generation: 1, - }, - spec: { - type: 'website', - }, - }, - { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { - name: 'component-service', - annotations: { - 'backstage.io/managed-by-location': location.id, - }, - uid: 'uid2', - etag: 'YTQzMmNmNjctMGZmMC00YWM5LWFjYWQtZDg1NjBjNDFlYWM4', - generation: 1, - }, - spec: { - type: 'service', - }, - }, - ], + entities, }; throw new Error(`'Location wasn't added: ${target}`); } diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index b92f07247f..1418ea17fa 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createApiRef } from '@backstage/core'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, Location } from '@backstage/catalog-model'; export const catalogApiRef = createApiRef({ id: 'plugin.catalog.service', @@ -28,5 +28,5 @@ export interface CatalogApi { addLocation( type: string, target: string, - ): Promise<{ location: any; entities: Entity[] }>; + ): Promise<{ location: Location; entities: Entity[] }>; } diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index c67dc158c8..0b6639f40a 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -26,12 +26,11 @@ import { DialogActions, Button, ListItem, - ListItemText, List, LinearProgress, - ListItemIcon, + Divider, + Link, } from '@material-ui/core'; -import LinkIcon from '@material-ui/icons/Link'; import { InfoCard, Page, @@ -40,8 +39,8 @@ import { ContentHeader, SupportButton, useApi, - alertApiRef, errorApiRef, + StructuredMetadataTable, } from '@backstage/core'; import RegisterComponentForm from '../RegisterComponentForm'; import { catalogApiRef } from '@backstage/plugin-catalog'; @@ -63,7 +62,6 @@ const useStyles = makeStyles(theme => ({ const FormStates = { Idle: 'idle', Success: 'success', - Error: 'error', Submitting: 'submitting', } as const; @@ -75,17 +73,14 @@ const RegisterComponentPage: FC<{}> = () => { FormStates.Idle, ); - const alertApi = useApi(alertApiRef); const errorApi = useApi(errorApiRef); const [result, setResult] = useState<{ data: any; error: null | Error; - loading: boolean; }>({ data: null, error: null, - loading: false, }); const handleSubmit = async (formData: Record) => { @@ -94,14 +89,11 @@ const RegisterComponentPage: FC<{}> = () => { try { const data = await catalogApi.addLocation('github', target); - alertApi.post({ - message: 'Successfully added the location', - severity: 'success', - }); - setResult({ error: null, loading: false, data }); + setResult({ error: null, data }); setFormState(FormStates.Success); } catch (e) { - setFormState(FormStates.Error); + setResult({ error: e, data: null }); + setFormState(FormStates.Idle); errorApi.post(e); } }; @@ -125,28 +117,40 @@ const RegisterComponentPage: FC<{}> = () => { setFormState(FormStates.Idle)} classes={{ paper: classes.dialogPaper }} > Component registration result - {result.data ? ( + {formState === FormStates.Success && ( <> Following components have been succefully created: - {result.data.entities.map((entity: any) => ( - - - - - - - - + {result.data.entities.map((entity: any, index: number) => ( + <> + + + /catalog/{entity.metadata.name} + + ), + }} + /> + + {index < result.data.entities.length - 1 && ( + + )} + ))} @@ -157,12 +161,6 @@ const RegisterComponentPage: FC<{}> = () => { - ) : ( - - - Your component is being created. Please wait. - - )} From f80c6304c21217c6507be5edbe117fcb0eddb578 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 2 Jun 2020 12:06:04 +0200 Subject: [PATCH 08/14] Use routeRefs --- .../components/CatalogPage/CatalogPage.tsx | 15 ++++++++---- .../components/CatalogTable/CatalogTable.tsx | 9 ++++++- plugins/catalog/src/index.ts | 1 + plugins/catalog/src/plugin.ts | 5 ++-- plugins/catalog/src/routes.ts | 14 +++++++++++ .../RegisterComponentPage.tsx | 24 ++++++++++++++----- plugins/register-component/src/index.ts | 2 +- plugins/register-component/src/plugin.ts | 10 ++++++-- .../src/components/ScaffolderPage/index.tsx | 2 +- plugins/scaffolder/src/index.ts | 2 +- plugins/scaffolder/src/plugin.ts | 10 ++++++-- 11 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 plugins/catalog/src/routes.ts diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 8a2b448d97..d3b29bfa86 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -34,6 +34,8 @@ import { } from '../CatalogFilter/CatalogFilter'; import { Button, makeStyles, Typography, Link } from '@material-ui/core'; import { filterGroups, defaultFilter } from '../../data/filters'; +import { Link as RouterLink } from 'react-router-dom'; +import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -72,18 +74,23 @@ const CatalogPage: FC<{}> = () => { 👋🏼 - {' '} + Welcome to Backstage, we are happy to have you. Start by checking - out our{' '} + out our getting started - {' '} + page. } /> - All your components diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index f5fa45dc7e..37c518cb6b 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -17,6 +17,8 @@ import React, { FC } from 'react'; import { Component } from '../../data/component'; import { InfoCard, Progress, Table, TableColumn } from '@backstage/core'; import { Typography, Link } from '@material-ui/core'; +import { Link as RouterLink, generatePath } from 'react-router-dom'; +import { entityRoute } from '../../routes'; const columns: TableColumn[] = [ { @@ -24,7 +26,12 @@ const columns: TableColumn[] = [ field: 'name', highlight: true, render: (componentData: any) => ( - {componentData.name} + + {componentData.name} + ), }, { diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index f495bd3146..2b986b59ea 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -18,3 +18,4 @@ export { plugin } from './plugin'; export * from './api/CatalogClient'; export * from './api/types'; export * from './types'; +export * from './routes'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 97e470a065..1eee5efe07 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -17,11 +17,12 @@ import { createPlugin } from '@backstage/core'; import CatalogPage from './components/CatalogPage'; import ComponentPage from './components/ComponentPage/ComponentPage'; +import { rootRoute, entityRoute } from './routes'; export const plugin = createPlugin({ id: 'catalog', register({ router }) { - router.registerRoute('/', CatalogPage); - router.registerRoute('/catalog/:name/', ComponentPage); + router.addRoute(rootRoute, CatalogPage); + router.addRoute(entityRoute, ComponentPage); }, }); diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts new file mode 100644 index 0000000000..79d02fefe1 --- /dev/null +++ b/plugins/catalog/src/routes.ts @@ -0,0 +1,14 @@ +import { createRouteRef } from '@backstage/core'; + +const NoIcon = () => null; + +export const rootRoute = createRouteRef({ + icon: NoIcon, + path: '/', + title: 'Catalog', +}); +export const entityRoute = createRouteRef({ + icon: NoIcon, + path: '/catalog/:name/', + title: 'Entity', +}); diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 0b6639f40a..d0189ba7f8 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -41,9 +41,15 @@ import { useApi, errorApiRef, StructuredMetadataTable, + Header, } from '@backstage/core'; import RegisterComponentForm from '../RegisterComponentForm'; import { catalogApiRef } from '@backstage/plugin-catalog'; +import { + entityRoute, + rootRoute as catalogRootRoute, +} from '@backstage/plugin-catalog'; +import { generatePath } from 'react-router'; const useStyles = makeStyles(theme => ({ dialogPaper: { @@ -100,10 +106,8 @@ const RegisterComponentPage: FC<{}> = () => { return ( +
- - Documentation - @@ -139,9 +143,13 @@ const RegisterComponentPage: FC<{}> = () => { link: ( - /catalog/{entity.metadata.name} + {generatePath(entityRoute.path, { + name: entity.metadata.name, + })} ), }} @@ -156,7 +164,11 @@ const RegisterComponentPage: FC<{}> = () => { - diff --git a/plugins/register-component/src/index.ts b/plugins/register-component/src/index.ts index 3a0a0fe2d3..5b20cb0158 100644 --- a/plugins/register-component/src/index.ts +++ b/plugins/register-component/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { plugin, rootRoute } from './plugin'; diff --git a/plugins/register-component/src/plugin.ts b/plugins/register-component/src/plugin.ts index f32e9dc84f..9c73688a70 100644 --- a/plugins/register-component/src/plugin.ts +++ b/plugins/register-component/src/plugin.ts @@ -14,12 +14,18 @@ * limitations under the License. */ -import { createPlugin } from '@backstage/core'; +import { createPlugin, createRouteRef } from '@backstage/core'; import RegisterComponentPage from './components/RegisterComponentPage'; +export const rootRoute = createRouteRef({ + icon: () => null, + path: '/register-component', + title: 'Register component', +}); + export const plugin = createPlugin({ id: 'register-component', register({ router }) { - router.registerRoute('/register-component', RegisterComponentPage); + router.addRoute(rootRoute, RegisterComponentPage); }, }); diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index 59eb6cff43..5d976a20b2 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -57,7 +57,7 @@ const ScaffolderPage: React.FC<{}> = () => { component={RouterLink} to="/register-component" > - Register existing entity + Register existing component diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 3a0a0fe2d3..5b20cb0158 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { plugin, rootRoute } from './plugin'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 31bdab2f84..b2cb305e57 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -14,12 +14,18 @@ * limitations under the License. */ -import { createPlugin } from '@backstage/core'; +import { createPlugin, createRouteRef } from '@backstage/core'; import ScaffolderPage from './components/ScaffolderPage'; +export const rootRoute = createRouteRef({ + icon: () => null, + path: '/create', + title: 'Create entity', +}); + export const plugin = createPlugin({ id: 'scaffolder', register({ router }) { - router.registerRoute('/create', ScaffolderPage); + router.addRoute(rootRoute, ScaffolderPage); }, }); From dfe817cb742c737ce773234c6e38f98633bff283 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 2 Jun 2020 13:55:17 +0200 Subject: [PATCH 09/14] Fix linting and tsc --- packages/app/package.json | 3 ++- packages/backend/src/index.ts | 12 ++++----- plugins/catalog/package.json | 9 ++++--- plugins/catalog/src/routes.ts | 26 ++++++++++++++++++- plugins/register-component/package.json | 3 +++ .../RegisterComponentForm.test.tsx | 13 +++------- .../RegisterComponentPage.tsx | 4 +-- plugins/scaffolder/package.json | 1 + 8 files changed, 48 insertions(+), 23 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 0af25d6f3a..15681d0548 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -12,15 +12,16 @@ "@backstage/plugin-lighthouse": "^0.1.1-alpha.6", "@backstage/plugin-register-component": "^0.1.1-alpha.6", "@backstage/plugin-scaffolder": "^0.1.1-alpha.6", + "@backstage/plugin-sentry": "^0.1.1-alpha.6", "@backstage/plugin-tech-radar": "^0.1.1-alpha.6", "@backstage/plugin-welcome": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", - "@backstage/plugin-sentry": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "prop-types": "^15.7.2", "react": "^16.12.0", "react-dom": "^16.12.0", + "react-hot-loader": "^4.12.21", "react-router-dom": "^5.2.0", "react-use": "^14.2.0", "zen-observable": "^0.8.15" diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a6299ca829..5b1fc54330 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -70,12 +70,12 @@ async function main() { app.use(requestLoggingHandler()); app.use('/catalog', await catalog(createEnv('catalog'))); app.use('/scaffolder', await scaffolder(createEnv('scaffolder'))); - // app.use( - // '/sentry', - // await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })), - // ); - // app.use('/auth', await auth(createEnv('auth'))); - // app.use('/identity', await identity(createEnv('identity'))); + app.use( + '/sentry', + await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })), + ); + app.use('/auth', await auth(createEnv('auth'))); + app.use('/identity', await identity(createEnv('identity'))); app.use(notFoundHandler()); app.use(errorHandler()); diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0cdc5d2618..5445fffb32 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -17,15 +17,18 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", "@backstage/catalog-model": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.6", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.6", + "@backstage/plugin-sentry": "^0.1.1-alpha.6", + "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "@backstage/plugin-sentry": "^0.1.1-alpha.6", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 79d02fefe1..830dc6d48f 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -1,4 +1,28 @@ -import { createRouteRef } from '@backstage/core'; +/* + * 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. + */ + +/* + * Hi! + * + * Note that this is an EXAMPLE Backstage backend. Please check the README. + * + * Happy hacking! + */ + + import { createRouteRef } from '@backstage/core'; const NoIcon = () => null; diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 053af6bb51..c2d8408f1f 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/plugin-catalog": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", @@ -25,6 +26,8 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-hook-form": "^5.7.2", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index 9203093d48..3b8b7f9dcf 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -18,11 +18,10 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import RegisterComponentForm from './RegisterComponentForm'; +// TODO(ishmidt): rewrite tests describe('RegisterComponentForm', () => { it('should initially render a disabled button', async () => { - const rendered = render( - , - ); + 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.', @@ -34,9 +33,7 @@ describe('RegisterComponentForm', () => { }); it('should enable a submit form when data when component url is set ', async () => { - const rendered = render( - , - ); + const rendered = render(); const input = (await rendered.getByRole('textbox')) as HTMLInputElement; fireEvent.change(input, { target: { value: 'https://example.com/blob/master/service.yaml' }, @@ -47,9 +44,7 @@ describe('RegisterComponentForm', () => { }); it('should hide input on submission ', async () => { - const rendered = render( - , - ); + const rendered = render(); expect( await rendered.findByText( diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index d0189ba7f8..6d01fc469e 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -36,18 +36,16 @@ import { Page, pageTheme, Content, - ContentHeader, - SupportButton, useApi, errorApiRef, StructuredMetadataTable, Header, } from '@backstage/core'; import RegisterComponentForm from '../RegisterComponentForm'; -import { catalogApiRef } from '@backstage/plugin-catalog'; import { entityRoute, rootRoute as catalogRootRoute, + catalogApiRef } from '@backstage/plugin-catalog'; import { generatePath } from 'react-router'; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 7f453fde48..ad44e06986 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -24,6 +24,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router-dom": "^5.2.0", "react-use": "^14.2.0" }, "devDependencies": { From 04566215693c6fa5d7ed16afc7503342df19083e Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 2 Jun 2020 14:20:40 +0200 Subject: [PATCH 10/14] Fix arguments order --- plugins/catalog-backend/src/service/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 22d1f73730..a3da182416 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -69,8 +69,8 @@ export async function createRouter( const { kind, namespace, name } = req.params; const entity = await entitiesCatalog.entityByName( kind, - name, namespace, + name, ); if (!entity) { res From 947cb2984ee0bda2eb5060ffc6f4181abbec57c6 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 2 Jun 2020 14:33:42 +0200 Subject: [PATCH 11/14] Show error from the backend --- plugins/catalog/src/api/CatalogClient.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 6784c08494..2e019982dc 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -43,7 +43,6 @@ export class CatalogClient implements CatalogApi { throw new Error(`'Entity not found: ${name}`); } - // TODO: Types for the type param async addLocation(type: string, target: string) { const response = await fetch( `${this.apiOrigin}${this.basePath}/locations`, @@ -55,16 +54,19 @@ export class CatalogClient implements CatalogApi { body: JSON.stringify({ type, target }), }, ); + if (response.status !== 201) { - throw new Error(`Location wasn't added: ${target}`); + throw new Error(await response.text()); } + const { location, entities } = await response.json(); - if (location && entities.length > 0) - return { - location, - entities, - }; - throw new Error(`'Location wasn't added: ${target}`); + if (!location || entities.length === 0) + throw new Error(`'Location wasn't added: ${target}`); + + return { + location, + entities, + }; } } From 3cc59f327674c9c0ca9d6237ee06a51e31336e76 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 4 Jun 2020 16:50:05 +0200 Subject: [PATCH 12/14] Fix tests --- .../CatalogPage/CatalogPage.test.tsx | 15 ++- plugins/register-component/package.json | 1 + .../RegisterComponentForm.test.tsx | 49 +++++--- .../RegisterComponentForm.tsx | 14 ++- .../RegisterComponentPage.test.tsx | 43 +++++-- .../RegisterComponentPage.tsx | 112 +++++------------- .../RegisterComponentResultDialog.test.tsx | 80 +++++++++++++ .../RegisterComponentResultDialog.tsx | 93 +++++++++++++++ .../RegisterComponentResultDialog/index.ts | 17 +++ 9 files changed, 306 insertions(+), 118 deletions(-) create mode 100644 plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.test.tsx create mode 100644 plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx create mode 100644 plugins/register-component/src/components/RegisterComponentResultDialog/index.ts 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'; From 163be0eafe08e4128639156632a9b1038e219c9c Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 4 Jun 2020 17:06:09 +0200 Subject: [PATCH 13/14] Addressed PR comments --- plugins/catalog/src/api/CatalogClient.ts | 2 +- plugins/catalog/src/api/types.ts | 7 +++---- plugins/catalog/src/routes.ts | 10 +--------- .../RegisterComponentResultDialog.test.tsx | 6 ++++-- .../RegisterComponentResultDialog.tsx | 4 ++-- 5 files changed, 11 insertions(+), 18 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 2e019982dc..429ae5f0a2 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -62,7 +62,7 @@ export class CatalogClient implements CatalogApi { const { location, entities } = await response.json(); if (!location || entities.length === 0) - throw new Error(`'Location wasn't added: ${target}`); + throw new Error(`Location wasn't added: ${target}`); return { location, diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 1418ea17fa..a01270e7d8 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -25,8 +25,7 @@ export const catalogApiRef = createApiRef({ export interface CatalogApi { getEntities(): Promise; getEntityByName(name: string): Promise; - addLocation( - type: string, - target: string, - ): Promise<{ location: Location; entities: Entity[] }>; + addLocation(type: string, target: string): Promise; } + +export type AddLocationResponse = { location: Location; entities: Entity[] }; diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 830dc6d48f..6498d412b8 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -/* - * Hi! - * - * Note that this is an EXAMPLE Backstage backend. Please check the README. - * - * Happy hacking! - */ - - import { createRouteRef } from '@backstage/core'; +import { createRouteRef } from '@backstage/core'; const NoIcon = () => null; diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.test.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.test.tsx index 284e83121c..d918e72489 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.test.tsx @@ -43,7 +43,7 @@ describe('RegisterComponentResultDialog', () => { it('should render', () => { const { rendered } = setup(); expect( - rendered.getByText('Component registration result'), + rendered.getByText('Component Registration Result'), ).toBeInTheDocument(); }); }); @@ -73,7 +73,9 @@ it('should show a list of components if success', async () => { }); expect( - rendered.getByText('Following components have been succefully created:'), + rendered.getByText( + 'The 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 index ead7323870..4d158526a0 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -48,10 +48,10 @@ export const RegisterComponentResultDialog: FC = ({ entities, }) => ( - Component registration result + Component Registration Result - Following components have been succefully created: + The following components have been succefully created: {entities.map((entity: any, index: number) => ( From f0a95a97623cf3a2b873352df6c4b75dd3df3428 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 4 Jun 2020 17:15:58 +0200 Subject: [PATCH 14/14] Fix types --- .../RegisterComponentPage/RegisterComponentPage.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx index d35691eadc..ad47c3a41b 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx @@ -29,6 +29,7 @@ const catalogApi: jest.Mocked = { addLocation: jest.fn((_a, _b) => new Promise(() => {})), getEntities: jest.fn(), getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), }; const setup = () => ({