diff --git a/.changeset/seven-tips-begin.md b/.changeset/seven-tips-begin.md
new file mode 100644
index 0000000000..b9ebc4f159
--- /dev/null
+++ b/.changeset/seven-tips-begin.md
@@ -0,0 +1,6 @@
+---
+'@backstage/create-app': patch
+'@backstage/plugin-api-docs': patch
+---
+
+Add tables with consumes and provides relationships to the API and component entity pages.
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 068019c622..f46e65d010 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -17,7 +17,10 @@ import { ApiEntity, Entity } from '@backstage/catalog-model';
import { EmptyState } from '@backstage/core';
import {
ApiDefinitionCard,
- Router as ApiDocsRouter,
+ ConsumedApisCard,
+ ConsumingComponentsCard,
+ ProvidedApisCard,
+ ProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
import {
AboutCard,
@@ -167,6 +170,17 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
);
+const ComponentApisContent = ({ entity }: { entity: Entity }) => (
+
+
+
+
+
+
+
+
+);
+
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
(
}
+ element={}
/>
(
+
+
+
+
+
+
+
+
);
diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
index 7d65b3261a..608a4f5f76 100644
--- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
@@ -13,26 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {
- Router as GitHubActionsRouter,
- isPluginApplicableToEntity as isGitHubActionsAvailable,
-} from '@backstage/plugin-github-actions';
-import {
- Router as CircleCIRouter,
- isPluginApplicableToEntity as isCircleCIAvailable,
-} from '@backstage/plugin-circleci';
-import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs';
-import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
-
-import React from 'react';
-import {
- EntityPageLayout,
- useEntity,
- AboutCard,
-} from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
-import { Grid } from '@material-ui/core';
import { WarningPanel } from '@backstage/core';
+import { ConsumedApisCard, ProvidedApisCard } from '@backstage/plugin-api-docs';
+import {
+ AboutCard, EntityPageLayout,
+ useEntity
+} from '@backstage/plugin-catalog';
+import {
+ isPluginApplicableToEntity as isCircleCIAvailable, Router as CircleCIRouter
+} from '@backstage/plugin-circleci';
+import {
+ isPluginApplicableToEntity as isGitHubActionsAvailable, Router as GitHubActionsRouter
+} from '@backstage/plugin-github-actions';
+import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
+import { Grid } from '@material-ui/core';
+import React from 'react';
+
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
@@ -60,6 +57,17 @@ const OverviewContent = ({ entity }: { entity: Entity }) => (
);
+const ComponentApisContent = ({ entity }: { entity: Entity }) => (
+
+
+
+
+
+
+
+
+);
+
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
(
}
+ element={}
/>
{
- const apiNames = useComponentApiNames(entity as ComponentEntity);
-
- const { apiEntities, loading } = useComponentApiEntities({
- entity: entity as ComponentEntity,
- });
-
- if (loading) {
- return ;
- }
-
- return (
-
- {apiNames.map(api => (
-
-
-
- ))}
-
- );
-};
diff --git a/plugins/api-docs/src/catalog/Router.tsx b/plugins/api-docs/src/catalog/Router.tsx
deleted file mode 100644
index 64c074fc46..0000000000
--- a/plugins/api-docs/src/catalog/Router.tsx
+++ /dev/null
@@ -1,40 +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 React from 'react';
-import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model';
-import { Route, Routes } from 'react-router';
-import { catalogRoute } from '../routes';
-import { EntityPageApi } from './EntityPageApi';
-import { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState';
-
-const isPluginApplicableToEntity = (entity: Entity) => {
- // TODO: Also support RELATION_CONSUMES_API
- return entity.relations?.some(r => r.type === RELATION_PROVIDES_API);
-};
-
-export const Router = ({ entity }: { entity: Entity }) =>
- !isPluginApplicableToEntity(entity) ? (
-
- ) : (
-
- }
- />
- )
-
- );
diff --git a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx
new file mode 100644
index 0000000000..7db62433eb
--- /dev/null
+++ b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { ApiEntity } from '@backstage/catalog-model';
+import { Table, TableColumn } from '@backstage/core';
+import React from 'react';
+import { ApiTypeTitle } from '../ApiDefinitionCard';
+import { EntityLink } from '../EntityLink';
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ field: 'metadata.name',
+ highlight: true,
+ render: (entity: any) => (
+ {entity.metadata.name}
+ ),
+ },
+ {
+ title: 'Owner',
+ field: 'spec.owner',
+ },
+ {
+ title: 'Lifecycle',
+ field: 'spec.lifecycle',
+ },
+ {
+ title: 'Type',
+ field: 'spec.type',
+ render: (entity: ApiEntity) => ,
+ },
+ {
+ title: 'Description',
+ field: 'metadata.description',
+ width: 'auto',
+ },
+];
+
+type Props = {
+ title: string;
+ variant?: string;
+ entities: (ApiEntity | undefined)[];
+};
+
+export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => {
+ const tableStyle: React.CSSProperties = {
+ minWidth: '0',
+ width: '100%',
+ };
+
+ if (variant === 'gridItem') {
+ tableStyle.height = 'calc(100% - 10px)';
+ }
+
+ return (
+
+ columns={columns}
+ title={title}
+ style={tableStyle}
+ options={{
+ // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
+ search: false,
+ paging: false,
+ actionsColumnIndex: -1,
+ padding: 'dense',
+ }}
+ // TODO: For now we skip all APIs that we can't find without a warning!
+ data={entities.filter(e => e !== undefined) as ApiEntity[]}
+ />
+ );
+};
diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
new file mode 100644
index 0000000000..a4a1e15511
--- /dev/null
+++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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, RELATION_CONSUMES_API } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { ApiDocsConfig, apiDocsConfigRef } from '../../config';
+import { ConsumedApisCard } from './ConsumedApisCard';
+
+describe('', () => {
+ const apiDocsConfig: jest.Mocked = {
+ getApiDefinitionWidget: jest.fn(),
+ } as any;
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ const apis = ApiRegistry.with(catalogApiRef, catalogApi).with(
+ apiDocsConfigRef,
+ apiDocsConfig,
+ );
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ namespace: 'my-namespace',
+ },
+ relations: [],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(getByText(/Consumed APIs/i)).toBeInTheDocument();
+ expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument();
+ });
+
+ it('shows consumed APIs', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ namespace: 'my-namespace',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'API',
+ namespace: 'my-namespace',
+ name: 'target-name',
+ },
+ type: RELATION_CONSUMES_API,
+ },
+ ],
+ };
+ catalogApi.getEntityByName.mockResolvedValue({
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ type: 'openapi',
+ owner: 'Test',
+ lifecycle: 'production',
+ definition: '...',
+ },
+ });
+ apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
+ type: 'openapi',
+ title: 'OpenAPI',
+ component: () => ,
+ });
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByText(/Consumed APIs/i)).toBeInTheDocument();
+ expect(getByText(/target-name/i)).toBeInTheDocument();
+ expect(getByText(/OpenAPI/)).toBeInTheDocument();
+ expect(getByText(/Test/i)).toBeInTheDocument();
+ expect(getByText(/production/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx
new file mode 100644
index 0000000000..0bd4919554
--- /dev/null
+++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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 {
+ ApiEntity,
+ Entity,
+ RELATION_CONSUMES_API,
+} from '@backstage/catalog-model';
+import { EmptyState, InfoCard, Progress } from '@backstage/core';
+import React, { PropsWithChildren } from 'react';
+import { ApisTable } from './ApisTable';
+import { MissingConsumesApisEmptyState } from '../EmptyState';
+import { useRelatedEntities } from '../useRelatedEntities';
+
+const ApisCard = ({
+ children,
+ variant = 'gridItem',
+}: PropsWithChildren<{ variant?: string }>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+type Props = {
+ entity: Entity;
+ variant?: string;
+};
+
+export const ConsumedApisCard = ({ entity, variant = 'gridItem' }: Props) => {
+ const { entities, loading, error } = useRelatedEntities(
+ entity,
+ RELATION_CONSUMES_API,
+ );
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ if (!entities || entities.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
new file mode 100644
index 0000000000..1f42ff9060
--- /dev/null
+++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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, RELATION_PROVIDES_API } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { ApiDocsConfig, apiDocsConfigRef } from '../../config';
+import { ProvidedApisCard } from './ProvidedApisCard';
+
+describe('', () => {
+ const apiDocsConfig: jest.Mocked = {
+ getApiDefinitionWidget: jest.fn(),
+ } as any;
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ const apis = ApiRegistry.with(catalogApiRef, catalogApi).with(
+ apiDocsConfigRef,
+ apiDocsConfig,
+ );
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ namespace: 'my-namespace',
+ },
+ relations: [],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(getByText(/Provided APIs/i)).toBeInTheDocument();
+ expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument();
+ });
+
+ it('shows consumed APIs', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-name',
+ namespace: 'my-namespace',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'API',
+ namespace: 'my-namespace',
+ name: 'target-name',
+ },
+ type: RELATION_PROVIDES_API,
+ },
+ ],
+ };
+ catalogApi.getEntityByName.mockResolvedValue({
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ type: 'openapi',
+ owner: 'Test',
+ lifecycle: 'production',
+ definition: '...',
+ },
+ });
+ apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
+ type: 'openapi',
+ title: 'OpenAPI',
+ component: () => ,
+ });
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByText(/Provided APIs/i)).toBeInTheDocument();
+ expect(getByText(/target-name/i)).toBeInTheDocument();
+ expect(getByText(/OpenAPI/)).toBeInTheDocument();
+ expect(getByText(/Test/i)).toBeInTheDocument();
+ expect(getByText(/production/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx
new file mode 100644
index 0000000000..618f2dc1f6
--- /dev/null
+++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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 {
+ ApiEntity,
+ Entity,
+ RELATION_PROVIDES_API,
+} from '@backstage/catalog-model';
+import { EmptyState, InfoCard, Progress } from '@backstage/core';
+import React, { PropsWithChildren } from 'react';
+import { ApisTable } from './ApisTable';
+import { MissingProvidesApisEmptyState } from '../EmptyState';
+import { useRelatedEntities } from '../useRelatedEntities';
+
+const ApisCard = ({
+ children,
+ variant = 'gridItem',
+}: PropsWithChildren<{ variant?: string }>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+type Props = {
+ entity: Entity;
+ variant?: string;
+};
+
+export const ProvidedApisCard = ({ entity, variant = 'gridItem' }: Props) => {
+ const { entities, loading, error } = useRelatedEntities(
+ entity,
+ RELATION_PROVIDES_API,
+ );
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ if (!entities || entities.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/plugins/api-docs/src/catalog/EntityPageApi/index.ts b/plugins/api-docs/src/components/ApisCards/index.ts
similarity index 84%
rename from plugins/api-docs/src/catalog/EntityPageApi/index.ts
rename to plugins/api-docs/src/components/ApisCards/index.ts
index 1d382e01de..2a01a1dc6e 100644
--- a/plugins/api-docs/src/catalog/EntityPageApi/index.ts
+++ b/plugins/api-docs/src/components/ApisCards/index.ts
@@ -14,4 +14,5 @@
* limitations under the License.
*/
-export { EntityPageApi } from './EntityPageApi';
+export { ConsumedApisCard } from './ConsumedApisCard';
+export { ProvidedApisCard } from './ProvidedApisCard';
diff --git a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx
new file mode 100644
index 0000000000..1b62a56d10
--- /dev/null
+++ b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 { ComponentEntity } from '@backstage/catalog-model';
+import { Table, TableColumn } from '@backstage/core';
+import React from 'react';
+import { EntityLink } from '../EntityLink';
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ field: 'metadata.name',
+ highlight: true,
+ render: (entity: any) => (
+ {entity.metadata.name}
+ ),
+ },
+ {
+ title: 'Owner',
+ field: 'spec.owner',
+ },
+ {
+ title: 'Lifecycle',
+ field: 'spec.lifecycle',
+ },
+ {
+ title: 'Type',
+ field: 'spec.type',
+ },
+ {
+ title: 'Description',
+ field: 'metadata.description',
+ width: 'auto',
+ },
+];
+
+type Props = {
+ title: string;
+ variant?: string;
+ entities: (ComponentEntity | undefined)[];
+};
+
+// TODO: In theory this could also be systems!
+export const ComponentsTable = ({
+ entities,
+ title,
+ variant = 'gridItem',
+}: Props) => {
+ const tableStyle: React.CSSProperties = {
+ minWidth: '0',
+ width: '100%',
+ };
+
+ if (variant === 'gridItem') {
+ tableStyle.height = 'calc(100% - 10px)';
+ }
+
+ return (
+
+ columns={columns}
+ title={title}
+ style={tableStyle}
+ options={{
+ // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
+ search: false,
+ paging: false,
+ actionsColumnIndex: -1,
+ padding: 'dense',
+ }}
+ // TODO: For now we skip all APIs that we can't find without a warning!
+ data={entities.filter(e => e !== undefined) as ComponentEntity[]}
+ />
+ );
+};
diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
new file mode 100644
index 0000000000..606ff7e77b
--- /dev/null
+++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
@@ -0,0 +1,125 @@
+/*
+ * 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, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { ConsumingComponentsCard } from './ConsumingComponentsCard';
+
+describe('', () => {
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ const apis = ApiRegistry.with(catalogApiRef, catalogApi);
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'my-name',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ type: 'openapi',
+ owner: 'Test',
+ lifecycle: 'production',
+ definition: '...',
+ },
+ relations: [],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(getByText(/Consumers/i)).toBeInTheDocument();
+ expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument();
+ });
+
+ it('shows consuming components', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'my-name',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ type: 'openapi',
+ owner: 'Test',
+ lifecycle: 'production',
+ definition: '...',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'Component',
+ namespace: 'my-namespace',
+ name: 'target-name',
+ },
+ type: RELATION_API_CONSUMED_BY,
+ },
+ ],
+ };
+ catalogApi.getEntityByName.mockResolvedValue({
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ type: 'service',
+ owner: 'Test',
+ lifecycle: 'production',
+ },
+ });
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByText(/Consumers/i)).toBeInTheDocument();
+ expect(getByText(/target-name/i)).toBeInTheDocument();
+ expect(getByText(/Test/i)).toBeInTheDocument();
+ expect(getByText(/production/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx
new file mode 100644
index 0000000000..0431367aa2
--- /dev/null
+++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 {
+ ComponentEntity,
+ Entity,
+ RELATION_API_CONSUMED_BY,
+} from '@backstage/catalog-model';
+import { EmptyState, InfoCard, Progress } from '@backstage/core';
+import React, { PropsWithChildren } from 'react';
+import { MissingConsumesApisEmptyState } from '../EmptyState';
+import { useRelatedEntities } from '../useRelatedEntities';
+import { ComponentsTable } from './ComponentsTable';
+
+const ComponentsCard = ({
+ children,
+ variant = 'gridItem',
+}: PropsWithChildren<{ variant?: string }>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+type Props = {
+ entity: Entity;
+ variant?: string;
+};
+
+export const ConsumingComponentsCard = ({
+ entity,
+ variant = 'gridItem',
+}: Props) => {
+ const { entities, loading, error } = useRelatedEntities(
+ entity,
+ RELATION_API_CONSUMED_BY,
+ );
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ if (!entities || entities.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
new file mode 100644
index 0000000000..d1cec1722a
--- /dev/null
+++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
@@ -0,0 +1,125 @@
+/*
+ * 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, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { ProvidingComponentsCard } from './ProvidingComponentsCard';
+
+describe('', () => {
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ const apis = ApiRegistry.with(catalogApiRef, catalogApi);
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'my-name',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ type: 'openapi',
+ owner: 'Test',
+ lifecycle: 'production',
+ definition: '...',
+ },
+ relations: [],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(getByText(/Providers/i)).toBeInTheDocument();
+ expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument();
+ });
+
+ it('shows providing components', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'my-name',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ type: 'openapi',
+ owner: 'Test',
+ lifecycle: 'production',
+ definition: '...',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'Component',
+ namespace: 'my-namespace',
+ name: 'target-name',
+ },
+ type: RELATION_API_PROVIDED_BY,
+ },
+ ],
+ };
+ catalogApi.getEntityByName.mockResolvedValue({
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ type: 'service',
+ owner: 'Test',
+ lifecycle: 'production',
+ },
+ });
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByText(/Providers/i)).toBeInTheDocument();
+ expect(getByText(/target-name/i)).toBeInTheDocument();
+ expect(getByText(/Test/i)).toBeInTheDocument();
+ expect(getByText(/production/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx
new file mode 100644
index 0000000000..9e405a3af3
--- /dev/null
+++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 {
+ ComponentEntity,
+ Entity,
+ RELATION_API_PROVIDED_BY,
+} from '@backstage/catalog-model';
+import { EmptyState, InfoCard, Progress } from '@backstage/core';
+import React, { PropsWithChildren } from 'react';
+import { MissingProvidesApisEmptyState } from '../EmptyState';
+import { useRelatedEntities } from '../useRelatedEntities';
+import { ComponentsTable } from './ComponentsTable';
+
+const ComponentsCard = ({
+ children,
+ variant = 'gridItem',
+}: PropsWithChildren<{ variant?: string }>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+type Props = {
+ entity: Entity;
+ variant?: string;
+};
+
+export const ProvidingComponentsCard = ({
+ entity,
+ variant = 'gridItem',
+}: Props) => {
+ const { entities, loading, error } = useRelatedEntities(
+ entity,
+ RELATION_API_PROVIDED_BY,
+ );
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ if (!entities || entities.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/plugins/api-docs/src/catalog/index.ts b/plugins/api-docs/src/components/ComponentsCards/index.ts
similarity index 81%
rename from plugins/api-docs/src/catalog/index.ts
rename to plugins/api-docs/src/components/ComponentsCards/index.ts
index 4c177df914..e1c0e87198 100644
--- a/plugins/api-docs/src/catalog/index.ts
+++ b/plugins/api-docs/src/components/ComponentsCards/index.ts
@@ -14,4 +14,5 @@
* limitations under the License.
*/
-export { Router } from './Router';
+export { ConsumingComponentsCard } from './ConsumingComponentsCard';
+export { ProvidingComponentsCard } from './ProvidingComponentsCard';
diff --git a/plugins/api-docs/src/components/useComponentApiNames.ts b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx
similarity index 57%
rename from plugins/api-docs/src/components/useComponentApiNames.ts
rename to plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx
index 1303967895..de753713c3 100644
--- a/plugins/api-docs/src/components/useComponentApiNames.ts
+++ b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx
@@ -14,16 +14,15 @@
* limitations under the License.
*/
-import {
- ComponentEntity,
- RELATION_PROVIDES_API,
-} from '@backstage/catalog-model';
+import { renderInTestApp } from '@backstage/test-utils';
+import React from 'react';
+import { MissingConsumesApisEmptyState } from './MissingConsumesApisEmptyState';
-export const useComponentApiNames = (entity: ComponentEntity) => {
- // TODO: This code doesn't handle namespaces and kinds correctly, but will be removed soon
- return (
- entity.relations
- ?.filter(r => r.type === RELATION_PROVIDES_API)
- ?.map(r => r.target.name) || []
- );
-};
+describe('', () => {
+ it('renders without exploding', async () => {
+ const { getByText } = await renderInTestApp(
+ ,
+ );
+ expect(getByText(/consumesApis:/i)).toBeInTheDocument();
+ });
+});
diff --git a/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx
new file mode 100644
index 0000000000..3e71168dde
--- /dev/null
+++ b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { Button, makeStyles, Typography } from '@material-ui/core';
+import { BackstageTheme } from '@backstage/theme';
+import { CodeSnippet, EmptyState } from '@backstage/core';
+
+const COMPONENT_YAML = `# Example
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: example
+spec:
+ type: service
+ lifecycle: production
+ owner: guest
+ consumesApis:
+ - example-api
+`;
+
+const useStyles = makeStyles(theme => ({
+ code: {
+ borderRadius: 6,
+ margin: `${theme.spacing(2)}px 0px`,
+ background: theme.palette.type === 'dark' ? '#444' : '#fff',
+ },
+}));
+
+export const MissingConsumesApisEmptyState = () => {
+ const classes = useStyles();
+ return (
+
+ Components can consume APIs that are displayed on this page. You need
+ to fill the consumesApis field to enable this tool.
+ >
+ }
+ action={
+ <>
+
+ Link an API to your component as shown in the highlighted example
+ below:
+
+
+
+
+
+ >
+ }
+ />
+ );
+};
diff --git a/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx
new file mode 100644
index 0000000000..b539753a95
--- /dev/null
+++ b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx
@@ -0,0 +1,28 @@
+/*
+ * 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 { renderInTestApp } from '@backstage/test-utils';
+import React from 'react';
+import { MissingProvidesApisEmptyState } from './MissingProvidesApisEmptyState';
+
+describe('', () => {
+ it('renders without exploding', async () => {
+ const { getByText } = await renderInTestApp(
+ ,
+ );
+ expect(getByText(/providesApis:/i)).toBeInTheDocument();
+ });
+});
diff --git a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx
similarity index 95%
rename from plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx
rename to plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx
index fbb8810088..9bf3465a34 100644
--- a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx
+++ b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx
@@ -40,12 +40,12 @@ const useStyles = makeStyles(theme => ({
},
}));
-export const MissingImplementsApisEmptyState = () => {
+export const MissingProvidesApisEmptyState = () => {
const classes = useStyles();
return (
Components can implement APIs that are displayed on this page. You
diff --git a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts b/plugins/api-docs/src/components/EmptyState/index.ts
similarity index 78%
rename from plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts
rename to plugins/api-docs/src/components/EmptyState/index.ts
index 1b7d35c0a2..d195c43eb1 100644
--- a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts
+++ b/plugins/api-docs/src/components/EmptyState/index.ts
@@ -14,4 +14,5 @@
* limitations under the License.
*/
-export { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState';
+export { MissingConsumesApisEmptyState } from './MissingConsumesApisEmptyState';
+export { MissingProvidesApisEmptyState } from './MissingProvidesApisEmptyState';
diff --git a/plugins/api-docs/src/components/index.ts b/plugins/api-docs/src/components/index.ts
index cf9b189091..cfd985f47c 100644
--- a/plugins/api-docs/src/components/index.ts
+++ b/plugins/api-docs/src/components/index.ts
@@ -14,13 +14,9 @@
* limitations under the License.
*/
-export type { ApiDefinitionWidget } from './ApiDefinitionCard';
-export {
- ApiDefinitionCard,
- defaultDefinitionWidgets,
-} from './ApiDefinitionCard';
-export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget';
-export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget';
-export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget';
-export { useComponentApiNames } from './useComponentApiNames';
-export { useComponentApiEntities } from './useComponentApiEntities';
+export * from './ApiDefinitionCard';
+export * from './ApisCards';
+export * from './AsyncApiDefinitionWidget';
+export * from './ComponentsCards';
+export * from './OpenApiDefinitionWidget';
+export * from './PlainApiDefinitionWidget';
diff --git a/plugins/api-docs/src/components/useComponentApiEntities.ts b/plugins/api-docs/src/components/useComponentApiEntities.ts
deleted file mode 100644
index 9e5cbd968e..0000000000
--- a/plugins/api-docs/src/components/useComponentApiEntities.ts
+++ /dev/null
@@ -1,83 +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 { useAsyncRetry } from 'react-use';
-import { errorApiRef, useApi } from '@backstage/core';
-import {
- ApiEntity,
- ComponentEntity,
- parseEntityName,
-} from '@backstage/catalog-model';
-import { catalogApiRef } from '@backstage/plugin-catalog';
-import { useComponentApiNames } from './useComponentApiNames';
-
-export function useComponentApiEntities({
- entity,
-}: {
- entity: ComponentEntity;
-}): {
- loading: boolean;
- apiEntities?: Map;
- error?: Error;
- retry: () => void;
-} {
- const catalogApi = useApi(catalogApiRef);
- const errorApi = useApi(errorApiRef);
-
- const apiNames = useComponentApiNames(entity);
-
- const { loading, value: apiEntities, retry, error } = useAsyncRetry<
- Map
- >(async () => {
- const resultMap = new Map();
-
- await Promise.all(
- apiNames.map(async name => {
- try {
- const apiEntityName = parseEntityName(name, {
- defaultNamespace: entity.metadata.namespace,
- defaultKind: 'API',
- });
-
- if (apiEntityName.kind !== 'API') {
- throw new Error(
- `Referenced entity of kind "${apiEntityName.kind}" as an API`,
- );
- }
-
- const api = (await catalogApi.getEntityByName(apiEntityName)) as
- | ApiEntity
- | undefined;
-
- if (api) {
- resultMap.set(api.metadata.name, api);
- }
- } catch (e) {
- errorApi.post(e);
- }
- }),
- );
-
- return resultMap;
- }, [catalogApi, entity]);
-
- return {
- apiEntities,
- loading,
- error,
- retry,
- };
-}
diff --git a/plugins/api-docs/src/components/useRelatedEntities.ts b/plugins/api-docs/src/components/useRelatedEntities.ts
new file mode 100644
index 0000000000..847ec30578
--- /dev/null
+++ b/plugins/api-docs/src/components/useRelatedEntities.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 { useApi } from '@backstage/core';
+import { catalogApiRef } from '@backstage/plugin-catalog';
+import { useAsyncRetry } from 'react-use';
+
+// TODO: Maybe this hook is interesting for others too?
+export function useRelatedEntities(
+ entity: Entity,
+ type: string,
+): {
+ entities: (Entity | undefined)[] | undefined;
+ loading: boolean;
+ error: Error | undefined;
+} {
+ const catalogApi = useApi(catalogApiRef);
+ const { loading, value, error } = useAsyncRetry<
+ (Entity | undefined)[]
+ >(async () => {
+ const relations =
+ entity.relations && entity.relations.filter(r => r.type === type);
+
+ if (!relations) {
+ return [];
+ }
+
+ return await Promise.all(
+ relations?.map(r => catalogApi.getEntityByName(r.target)),
+ );
+ }, [entity, type]);
+
+ return {
+ entities: value,
+ loading,
+ error,
+ };
+}
diff --git a/plugins/api-docs/src/index.ts b/plugins/api-docs/src/index.ts
index dbb32cee7b..f09aeb1038 100644
--- a/plugins/api-docs/src/index.ts
+++ b/plugins/api-docs/src/index.ts
@@ -14,6 +14,5 @@
* limitations under the License.
*/
-export * from './catalog';
export * from './components';
export { plugin } from './plugin';
diff --git a/plugins/api-docs/src/routes.ts b/plugins/api-docs/src/routes.ts
index 6adff78e47..64277b9ae8 100644
--- a/plugins/api-docs/src/routes.ts
+++ b/plugins/api-docs/src/routes.ts
@@ -23,9 +23,3 @@ export const rootRoute = createRouteRef({
path: '/api-docs',
title: 'APIs',
});
-
-export const catalogRoute = createRouteRef({
- icon: NoIcon,
- path: '',
- title: 'API',
-});