diff --git a/.changeset/itchy-kiwis-unite.md b/.changeset/itchy-kiwis-unite.md new file mode 100644 index 0000000000..a17f3072b2 --- /dev/null +++ b/.changeset/itchy-kiwis-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Extracted generic EntityExplorerContent component so that is is easy to show any component kinds in their own tab in the Explore page. diff --git a/plugins/explore/README.md b/plugins/explore/README.md index 0204f34714..f60d83eafb 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -2,7 +2,7 @@ Welcome to the explore plugin! -This plugin helps to visualize the domains and tools in your ecosystem. +This plugin helps to visualize the domains, systems and tools in your ecosystem. ## Setup @@ -116,7 +116,10 @@ export const ExplorePage = () => { subtitle="Browse our ecosystem" > - + + + + diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx index 54beb59619..49a79f5143 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.tsx @@ -15,11 +15,11 @@ */ import React from 'react'; -import { DomainExplorerContent } from '../DomainExplorerContent'; import { ExploreLayout } from '../ExploreLayout'; import { GroupsExplorerContent } from '../GroupsExplorerContent'; import { ToolExplorerContent } from '../ToolExplorerContent'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { EntityExplorerContent } from '../EntityExplorerContent'; export const DefaultExplorePage = () => { const configApi = useApi(configApiRef); @@ -32,7 +32,10 @@ export const DefaultExplorePage = () => { subtitle="Discover solutions available in your ecosystem" > - + + + + diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx index 3cfed5b12b..15c0d650cc 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx @@ -19,7 +19,6 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Button } from '@material-ui/core'; import React from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { DomainCard } from '../DomainCard'; import { Content, ContentHeader, @@ -30,6 +29,7 @@ import { WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +import { EntityCard } from '../EntityCard'; const Body = () => { const catalogApi = useApi(catalogApiRef); @@ -78,7 +78,7 @@ const Body = () => { return ( {entities.map((entity, index) => ( - + ))} ); diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx similarity index 93% rename from plugins/explore/src/components/DomainCard/DomainCard.test.tsx rename to plugins/explore/src/components/EntityCard/EntityCard.test.tsx index df9b84820a..ccf5deb875 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx +++ b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx @@ -18,9 +18,9 @@ import { DomainEntity } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { DomainCard } from './DomainCard'; +import { EntityCard } from './EntityCard'; -describe('', () => { +describe('', () => { it('renders a domain card', async () => { const entity: DomainEntity = { apiVersion: 'backstage.io/v1alpha1', @@ -35,7 +35,7 @@ describe('', () => { }, }; const { getByText } = await renderInTestApp( - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/EntityCard/EntityCard.tsx similarity index 93% rename from plugins/explore/src/components/DomainCard/DomainCard.tsx rename to plugins/explore/src/components/EntityCard/EntityCard.tsx index a20a5e1f0c..a9ba4f2550 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/EntityCard/EntityCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { EntityRefLinks, entityRouteParams, @@ -35,7 +35,7 @@ import { Button, ItemCardHeader } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; /** @public */ -export const DomainCard = (props: { entity: DomainEntity }) => { +export const EntityCard = (props: { entity: Entity }) => { const { entity } = props; const catalogEntityRoute = useRouteRef(entityRouteRef); diff --git a/plugins/explore/src/components/DomainCard/index.ts b/plugins/explore/src/components/EntityCard/index.ts similarity index 93% rename from plugins/explore/src/components/DomainCard/index.ts rename to plugins/explore/src/components/EntityCard/index.ts index dbae755460..be80fb3f8f 100644 --- a/plugins/explore/src/components/DomainCard/index.ts +++ b/plugins/explore/src/components/EntityCard/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { DomainCard } from './DomainCard'; +export { EntityCard } from './EntityCard'; diff --git a/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx b/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx new file mode 100644 index 0000000000..4670a10855 --- /dev/null +++ b/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { DomainEntity } from '@backstage/catalog-model'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { EntityExplorerContent } from './EntityExplorerContent'; + +describe('', () => { + const catalogApi = { + addLocation: jest.fn(), + getEntities: jest.fn(), + getLocationByRef: jest.fn(), + getLocationById: jest.fn(), + removeLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + getEntityByRef: jest.fn(), + refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), + validateEntity: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + + const mountedRoutes = { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders a grid of domains', async () => { + const entities: DomainEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'playback', + }, + spec: { + owner: 'guest', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'artists', + }, + spec: { + owner: 'guest', + }, + }, + ]; + catalogApi.getEntities.mockResolvedValue({ items: entities }); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => { + expect(getByText('artists')).toBeInTheDocument(); + expect(getByText('playback')).toBeInTheDocument(); + }); + }); + + it('renders a custom title', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => expect(getByText('Our Areas')).toBeInTheDocument()); + }); + + it('renders empty state', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => + expect(getByText('No domains to display')).toBeInTheDocument(), + ); + }); + + it('renders a friendly error if it cannot collect domains', async () => { + const catalogError = new Error('Network timeout'); + catalogApi.getEntities.mockRejectedValueOnce(catalogError); + + const { getByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + await waitFor(() => + expect(getByText(/Could not load domains/)).toBeInTheDocument(), + ); + }); +}); diff --git a/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx b/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx new file mode 100644 index 0000000000..55ef13d98e --- /dev/null +++ b/plugins/explore/src/components/EntityExplorerContent/EntityExplorerContent.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Button } from '@material-ui/core'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { EntityCard } from '../EntityCard'; +import { + Content, + ContentHeader, + EmptyState, + ItemCardGrid, + Progress, + SupportButton, + WarningPanel, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; + +const Body = (props: { kind: string }) => { + const { kind } = props; + const kindPlural = `${kind}s`; + const catalogApi = useApi(catalogApiRef); + const { + value: entities, + loading, + error, + } = useAsync(async () => { + const response = await catalogApi.getEntities({ + filter: { kind }, + }); + return response.items as Entity[]; + }, [catalogApi]); + + if (loading) { + return ; + } + + if (error) { + return ( + + {error.message} + + ); + } + + if (!entities?.length) { + return ( + + Read more + + } + /> + ); + } + + return ( + + {entities.map((entity, index) => ( + + ))} + + ); +}; + +/** @public */ +export const EntityExplorerContent = (props: { + tabTitle?: string; + kind: string; +}) => { + const { kind, tabTitle } = props; + return ( + + + + Discover the {kind.toLocaleLowerCase()}s in your ecosystem. + + + + + ); +}; diff --git a/plugins/explore/src/components/EntityExplorerContent/index.ts b/plugins/explore/src/components/EntityExplorerContent/index.ts new file mode 100644 index 0000000000..ab06193939 --- /dev/null +++ b/plugins/explore/src/components/EntityExplorerContent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { EntityExplorerContent } from './EntityExplorerContent';