From 3534c0804c021514009542e09848ec7e0000512f Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Fri, 24 Jul 2020 15:36:04 +0200 Subject: [PATCH] feat(api-docs): add routes to display all api entities --- .../ApiCatalogLayout.tsx} | 35 ++--- .../ApiCatalogPage/ApiCatalogPage.test.tsx | 70 ++++++++++ .../ApiCatalogPage/ApiCatalogPage.tsx | 45 ++++++ .../ApiCatalogTable/ApiCatalogTable.test.tsx | 74 ++++++++++ .../ApiCatalogTable/ApiCatalogTable.tsx | 89 ++++++++++++ .../ApiEntityPage/ApiEntityPage.test.tsx | 102 ++++++++++++++ .../ApiEntityPage/ApiEntityPage.tsx | 131 ++++++++++++++++++ .../CatalogApiPluginPage.tsx | 56 -------- plugins/api-docs/src/plugin.ts | 14 +- .../index.ts => routes.ts} | 15 +- 10 files changed, 550 insertions(+), 81 deletions(-) rename plugins/api-docs/src/components/{CatalogApiPluginPage/CatalogApiPluginPage.test.tsx => ApiCatalogPage/ApiCatalogLayout.tsx} (51%) create mode 100644 plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx create mode 100644 plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx create mode 100644 plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx create mode 100644 plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx create mode 100644 plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx create mode 100644 plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx delete mode 100644 plugins/api-docs/src/components/CatalogApiPluginPage/CatalogApiPluginPage.tsx rename plugins/api-docs/src/{components/CatalogApiPluginPage/index.ts => routes.ts} (65%) diff --git a/plugins/api-docs/src/components/CatalogApiPluginPage/CatalogApiPluginPage.test.tsx b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx similarity index 51% rename from plugins/api-docs/src/components/CatalogApiPluginPage/CatalogApiPluginPage.test.tsx rename to plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx index 4bb39b6e8c..d846e2b0e2 100644 --- a/plugins/api-docs/src/components/CatalogApiPluginPage/CatalogApiPluginPage.test.tsx +++ b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogLayout.tsx @@ -14,21 +14,24 @@ * limitations under the License. */ -import { lightTheme } from '@backstage/theme'; -import { ThemeProvider } from '@material-ui/core'; -import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; +import { Header, Page, pageTheme } from '@backstage/core'; import React from 'react'; -import CatalogApiPluginPage from './CatalogApiPluginPage'; -describe('CatalogApiPluginPage', () => { - it('should render', () => { - mockFetch.mockResponse(() => new Promise(() => {})); - const rendered = render( - - - , - ); - expect(rendered.getByText('Welcome to catalog-api!')).toBeInTheDocument(); - }); -}); +type Props = { + children?: React.ReactNode; +}; + +const ApiCatalogLayout = ({ children }: Props) => { + return ( + +
+ {children} + + ); +}; + +export default ApiCatalogLayout; diff --git a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx new file mode 100644 index 0000000000..19cbaf6038 --- /dev/null +++ b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core'; +// TODO: Circular ref! +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { ApiCatalogPage } from './ApiCatalogPage'; + +describe('ApiCatalogPage', () => { + const catalogApi: Partial = { + getEntities: () => + Promise.resolve([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { + name: 'Entity1', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { + name: 'Entity2', + }, + }, + ] as Entity[]), + getLocationByEntity: () => + Promise.resolve({ id: 'id', type: 'github', target: 'url' }), + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children} + , + ), + ); + + // this test right now causes some red lines in the log output when running tests + // related to some theme issues in mui-table + // https://github.com/mbrn/material-table/issues/1293 + it('should render', async () => { + const { findByText } = renderWrapped(); + expect(await findByText(/APIs \(2\)/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx new file mode 100644 index 0000000000..f624ec9f92 --- /dev/null +++ b/plugins/api-docs/src/components/ApiCatalogPage/ApiCatalogPage.tsx @@ -0,0 +1,45 @@ +/* + * 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 { Content, useApi } from '@backstage/core'; +// TODO: Circular ref +import { catalogApiRef } from '@backstage/plugin-catalog'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { ApiCatalogTable } from '../ApiCatalogTable/ApiCatalogTable'; +import ApiCatalogLayout from './ApiCatalogLayout'; + +const CatalogPageContents = () => { + const catalogApi = useApi(catalogApiRef); + const { loading, error, value: matchingEntities } = useAsync(() => { + return catalogApi.getEntities({ kind: 'API' }); + }, [catalogApi]); + + return ( + + + + + + ); +}; + +export const ApiCatalogPage = () => ; diff --git a/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx b/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx new file mode 100644 index 0000000000..67f6562a56 --- /dev/null +++ b/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { ApiCatalogTable } from './ApiCatalogTable'; + +const entites: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { name: 'api1' }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { name: 'api2' }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { name: 'api3' }, + }, +]; + +describe('ApiCatalogTable component', () => { + it('should render error message when error is passed in props', async () => { + const rendered = render( + wrapInTestApp( + , + ), + ); + const errorMessage = await rendered.findByText( + /Error encountered while fetching catalog entities./, + ); + expect(errorMessage).toBeInTheDocument(); + }); + + it('should display entity names when loading has finished and no error occurred', async () => { + const rendered = render( + wrapInTestApp( + , + ), + ); + expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument(); + expect(rendered.getByText(/api1/)).toBeInTheDocument(); + expect(rendered.getByText(/api2/)).toBeInTheDocument(); + expect(rendered.getByText(/api3/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx b/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx new file mode 100644 index 0000000000..e30583caed --- /dev/null +++ b/plugins/api-docs/src/components/ApiCatalogTable/ApiCatalogTable.tsx @@ -0,0 +1,89 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { Table, TableColumn } from '@backstage/core'; +import { Link } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React from 'react'; +import { generatePath, Link as RouterLink } from 'react-router-dom'; +import { entityRoute } from '../../routes'; + +const columns: TableColumn[] = [ + { + title: 'Name', + field: 'metadata.name', + highlight: true, + render: (entity: any) => ( + + {entity.metadata.name} + + ), + }, + { + title: 'Description', + field: 'metadata.description', + }, +]; + +type CatalogTableProps = { + entities: Entity[]; + titlePreamble: string; + loading: boolean; + error?: any; +}; + +export const ApiCatalogTable = ({ + entities, + loading, + error, + titlePreamble, +}: CatalogTableProps) => { + if (error) { + return ( +
+ + Error encountered while fetching catalog entities. {error.toString()} + +
+ ); + } + + return ( + + isLoading={loading} + columns={columns} + options={{ + paging: false, + actionsColumnIndex: -1, + loadingType: 'linear', + showEmptyDataSourceMessage: !loading, + }} + title={`${titlePreamble} (${(entities && entities.length) || 0})`} + data={entities} + /> + ); +}; diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx new file mode 100644 index 0000000000..af5213e3f1 --- /dev/null +++ b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { render, waitFor } from '@testing-library/react'; +import * as React from 'react'; +import { ApiEntityPage, getPageTheme } from './ApiEntityPage'; + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + const mockNavigate = jest.fn(); + return { + ...actual, + useNavigate: jest.fn(() => mockNavigate), + useParams: jest.fn(), + }; +}); + +const { + useParams, + useNavigate, +}: { useParams: jest.Mock; useNavigate: () => jest.Mock } = jest.requireMock( + 'react-router-dom', +); + +const errorApi = { post: () => {} }; + +describe('ApiEntityPage', () => { + it('should redirect to catalog page when name is not provided', async () => { + useParams.mockReturnValue({ + kind: 'Component', + optionalNamespaceAndName: '', + }); + + render( + wrapInTestApp( + ) as CatalogApi, + ], + ])} + > + + , + ), + ); + + await waitFor(() => + expect(useNavigate()).toHaveBeenCalledWith('/api-docs'), + ); + }); +}); + +describe('getPageTheme', () => { + const defaultPageTheme = getPageTheme(); + it.each(['service', 'app', 'library', 'tool', 'documentation', 'website'])( + 'should select right theme for predefined type: %p ̰ ', + type => { + const theme = getPageTheme(({ + spec: { + type, + }, + } as any) as Entity); + expect(theme).toBeDefined(); + expect(theme).not.toBe(defaultPageTheme); + }, + ); + + it('should select default theme for unknown/unspecified types', () => { + const theme1 = getPageTheme(({ + spec: { + type: 'unknown-type', + }, + } as any) as Entity); + const theme2 = getPageTheme(({ + spec: {}, + } as any) as Entity); + expect(theme1).toBe(defaultPageTheme); + expect(theme2).toBe(defaultPageTheme); + }); +}); diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx new file mode 100644 index 0000000000..5e08af7e8e --- /dev/null +++ b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx @@ -0,0 +1,131 @@ +/* + * 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 { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model'; +import { + Content, + errorApiRef, + Header, + Page, + pageTheme, + PageTheme, + Progress, + useApi, +} from '@backstage/core'; +// TODO: Circular ref +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { Box } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React, { FC, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useAsync } from 'react-use'; +import { ApiDefinitionCard } from '../ApiDefinitionCard/ApiDefinitionCard'; + +const REDIRECT_DELAY = 1000; +function headerProps( + kind: string, + namespace: string | undefined, + name: string, + entity: Entity | undefined, +): { headerTitle: string; headerType: string } { + return { + headerTitle: `${name}${namespace ? ` in ${namespace}` : ''}`, + headerType: (() => { + let t = kind.toLowerCase(); + if (entity && entity.spec && 'type' in entity.spec) { + t += ' — '; + t += (entity.spec as { type: string }).type.toLowerCase(); + } + return t; + })(), + }; +} + +export const getPageTheme = (entity?: Entity): PageTheme => { + const themeKey = entity?.spec?.type?.toString() ?? 'home'; + return pageTheme[themeKey] ?? pageTheme.home; +}; + +const EntityPageTitle: FC<{ title: string; entity: Entity | undefined }> = ({ + title, +}) => ( + + {title} + +); + +export const ApiEntityPage: FC<{}> = () => { + const { optionalNamespaceAndName } = useParams() as { + optionalNamespaceAndName: string; + }; + const navigate = useNavigate(); + const [name, namespace] = optionalNamespaceAndName.split(':').reverse(); + + const errorApi = useApi(errorApiRef); + const catalogApi = useApi(catalogApiRef); + + const { value: entity, error, loading } = useAsync( + () => catalogApi.getEntityByName({ kind: 'API', namespace, name }), + [catalogApi, namespace, name], + ); + + useEffect(() => { + if (!error && !loading && !entity) { + errorApi.post(new Error('Entity not found!')); + setTimeout(() => { + navigate('/'); + }, REDIRECT_DELAY); + } + }, [errorApi, navigate, error, loading, entity]); + + if (!name) { + navigate('/api-docs'); + return null; + } + + const { headerTitle, headerType } = headerProps( + 'API', + namespace, + name, + entity, + ); + + return ( + +
} + pageTitleOverride={headerTitle} + type={headerType} + /> + + {loading && } + + {error && ( + + {error.toString()} + + )} + + {entity && ( + <> + + + + + )} + + ); +}; diff --git a/plugins/api-docs/src/components/CatalogApiPluginPage/CatalogApiPluginPage.tsx b/plugins/api-docs/src/components/CatalogApiPluginPage/CatalogApiPluginPage.tsx deleted file mode 100644 index dc47a74052..0000000000 --- a/plugins/api-docs/src/components/CatalogApiPluginPage/CatalogApiPluginPage.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 { - Content, - ContentHeader, - Header, - HeaderLabel, - InfoCard, - Page, - pageTheme, - SupportButton, -} from '@backstage/core'; -import { Grid, Typography } from '@material-ui/core'; -import React, { FC } from 'react'; - -const CatalogApiPluginPage: FC<{}> = () => ( - -
- - -
- - - A description of your plugin goes here. - - - - - - All content should be wrapped in a card like this. - - - - -

API Catalog

-
-
-
-
-); - -export default CatalogApiPluginPage; diff --git a/plugins/api-docs/src/plugin.ts b/plugins/api-docs/src/plugin.ts index 90d8990aaa..db58538b67 100644 --- a/plugins/api-docs/src/plugin.ts +++ b/plugins/api-docs/src/plugin.ts @@ -14,17 +14,15 @@ * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; -import CatalogApiPluginPage from './components/CatalogApiPluginPage'; - -export const rootRouteRef = createRouteRef({ - path: '/api-docs', - title: 'api-docs', -}); +import { createPlugin } from '@backstage/core'; +import { ApiCatalogPage } from './components/ApiCatalogPage/ApiCatalogPage'; +import { ApiEntityPage } from './components/ApiEntityPage/ApiEntityPage'; +import { entityRoute, rootRoute } from './routes'; export const plugin = createPlugin({ id: 'api-docs', register({ router }) { - router.addRoute(rootRouteRef, CatalogApiPluginPage); + router.addRoute(rootRoute, ApiCatalogPage); + router.addRoute(entityRoute, ApiEntityPage); }, }); diff --git a/plugins/api-docs/src/components/CatalogApiPluginPage/index.ts b/plugins/api-docs/src/routes.ts similarity index 65% rename from plugins/api-docs/src/components/CatalogApiPluginPage/index.ts rename to plugins/api-docs/src/routes.ts index 7021d2bf9f..48c530d4b5 100644 --- a/plugins/api-docs/src/components/CatalogApiPluginPage/index.ts +++ b/plugins/api-docs/src/routes.ts @@ -14,4 +14,17 @@ * limitations under the License. */ -export { default } from './CatalogApiPluginPage'; +import { createRouteRef } from '@backstage/core'; + +const NoIcon = () => null; + +export const rootRoute = createRouteRef({ + icon: NoIcon, + path: '/api-docs', + title: 'APIs', +}); +export const entityRoute = createRouteRef({ + icon: NoIcon, + path: '/api-docs/:optionalNamespaceAndName/', + title: 'API', +});