diff --git a/.changeset/friendly-readers-promise.md b/.changeset/friendly-readers-promise.md
new file mode 100644
index 0000000000..30607d5db2
--- /dev/null
+++ b/.changeset/friendly-readers-promise.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+Fetch relations in batches in `useRelatedEntities`
diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
index b157e898ca..fa956cd042 100644
--- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
@@ -96,14 +96,18 @@ describe('', () => {
},
],
};
- catalogApi.getEntityByName.mockResolvedValue({
- apiVersion: 'v1',
- kind: 'API',
- metadata: {
- name: 'target-name',
- namespace: 'my-namespace',
- },
- spec: {},
+ catalogApi.getEntities.mockResolvedValue({
+ items: [
+ {
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ },
+ ],
});
const { getByText } = await renderInTestApp(
diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx
index 5db3ec2b48..e721f6e918 100644
--- a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx
+++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx
@@ -96,14 +96,18 @@ describe('', () => {
},
],
};
- catalogApi.getEntityByName.mockResolvedValue({
- apiVersion: 'v1',
- kind: 'API',
- metadata: {
- name: 'target-name',
- namespace: 'my-namespace',
- },
- spec: {},
+ catalogApi.getEntities.mockResolvedValue({
+ items: [
+ {
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ },
+ ],
});
const { getByText } = await renderInTestApp(
diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
index bb9b2e60d9..67d95605e4 100644
--- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
@@ -96,14 +96,18 @@ describe('', () => {
},
],
};
- catalogApi.getEntityByName.mockResolvedValue({
- apiVersion: 'v1',
- kind: 'API',
- metadata: {
- name: 'target-name',
- namespace: 'my-namespace',
- },
- spec: {},
+ catalogApi.getEntities.mockResolvedValue({
+ items: [
+ {
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ },
+ ],
});
const { getByText } = await renderInTestApp(
diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
index 7944b3f90a..104b360ff6 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
+++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
@@ -101,14 +101,18 @@ describe('', () => {
},
],
};
- catalogApi.getEntityByName.mockResolvedValue({
- apiVersion: 'v1',
- kind: 'Component',
- metadata: {
- name: 'target-name',
- namespace: 'my-namespace',
- },
- spec: {},
+ catalogApi.getEntities.mockResolvedValue({
+ items: [
+ {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ },
+ ],
});
const { getByText } = await renderInTestApp(
diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
index ab1751b9ba..c7c6bacc84 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
+++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
@@ -101,14 +101,18 @@ describe('', () => {
},
],
};
- catalogApi.getEntityByName.mockResolvedValue({
- apiVersion: 'v1',
- kind: 'Component',
- metadata: {
- name: 'target-name',
- namespace: 'my-namespace',
- },
- spec: {},
+ catalogApi.getEntities.mockResolvedValue({
+ items: [
+ {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ },
+ ],
});
const { getByText } = await renderInTestApp(
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json
index 91201a49a1..57a9479a3c 100644
--- a/plugins/catalog-react/package.json
+++ b/plugins/catalog-react/package.json
@@ -33,6 +33,7 @@
"@backstage/core": "^0.7.3",
"@material-ui/core": "^4.11.0",
"@types/react": "^16.9",
+ "lodash": "^4.17.15",
"react": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
diff --git a/plugins/catalog-react/src/hooks/useRelatedEntities.ts b/plugins/catalog-react/src/hooks/useRelatedEntities.ts
index baf6cf5fc7..b47d941ff6 100644
--- a/plugins/catalog-react/src/hooks/useRelatedEntities.ts
+++ b/plugins/catalog-react/src/hooks/useRelatedEntities.ts
@@ -13,11 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Entity } from '@backstage/catalog-model';
+import { Entity, EntityRelation } from '@backstage/catalog-model';
import { useApi } from '@backstage/core';
+import { chunk, groupBy } from 'lodash';
import { useAsync } from 'react-use';
import { catalogApiRef } from '../api';
+const BATCH_SIZE = 20;
+
export function useRelatedEntities(
entity: Entity,
{ type, kind }: { type?: string; kind?: string },
@@ -40,16 +43,50 @@ export function useRelatedEntities(
return [];
}
- // TODO: This code could be more efficient if there was an endpoint in the
- // backend that either returns the relations of entity (filtered by type)
- // or if there is a way to perform a batch request by entity name. However,
- // such an implementation would probably be better placed in the graphql API.
- const results = await Promise.all(
- relations?.map(r => catalogApi.getEntityByName(r.target)),
+ // Group the relations by kind and namespace to reduce the size of the request query string.
+ // Without this grouping, the kind and namespace would need to be specified for each relation, e.g.
+ // `filter=kind=component,namespace=default,name=example1&filter=kind=component,namespace=default,name=example2`
+ // with grouping, we can generate a query a string like
+ // `filter=kind=component,namespace=default,name=example1,example2`
+ const relationsByKindAndNamespace: EntityRelation[][] = Object.values(
+ groupBy(relations, ({ target }) => {
+ return `${target.kind}:${target.namespace}`.toLowerCase();
+ }),
);
- // Skip entities that where not found, for example if a relation references
- // an entity that doesn't exist.
- return results.filter(e => e) as Entity[];
+
+ // Split the names within each group into batches to further reduce the query string length.
+ const batchedRelationsByKindAndNamespace: {
+ kind: string;
+ namespace: string;
+ nameBatches: string[][];
+ }[] = [];
+ for (const rs of relationsByKindAndNamespace) {
+ batchedRelationsByKindAndNamespace.push({
+ // All relations in a group have the same kind and namespace, so its arbitrary which we pick
+ kind: rs[0].target.kind,
+ namespace: rs[0].target.namespace,
+ nameBatches: chunk(
+ rs.map(r => r.target.name),
+ BATCH_SIZE,
+ ),
+ });
+ }
+
+ const results = await Promise.all(
+ batchedRelationsByKindAndNamespace.flatMap(rs => {
+ return rs.nameBatches.map(names => {
+ return catalogApi.getEntities({
+ filter: {
+ kind: rs.kind,
+ 'metadata.namespace': rs.namespace,
+ 'metadata.name': names,
+ },
+ });
+ });
+ }),
+ );
+
+ return results.flatMap(r => r.items);
}, [entity, type]);
return {
diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx
index 58df0a53f8..8e0a61b05a 100644
--- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx
+++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx
@@ -91,14 +91,18 @@ describe('', () => {
},
],
};
- catalogApi.getEntityByName.mockResolvedValue({
- apiVersion: 'v1',
- kind: 'Component',
- metadata: {
- name: 'target-name',
- namespace: 'my-namespace',
- },
- spec: {},
+ catalogApi.getEntities.mockResolvedValue({
+ items: [
+ {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ },
+ ],
});
const { getByText } = await renderInTestApp(
diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx
index 79b69d35ff..9ad69c9a0c 100644
--- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx
+++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx
@@ -91,14 +91,18 @@ describe('', () => {
},
],
};
- catalogApi.getEntityByName.mockResolvedValue({
- apiVersion: 'v1',
- kind: 'Component',
- metadata: {
- name: 'target-name',
- namespace: 'my-namespace',
- },
- spec: {},
+ catalogApi.getEntities.mockResolvedValue({
+ items: [
+ {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ },
+ ],
});
const { getByText } = await renderInTestApp(
diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx
index 08923a59c8..9b7fb0adae 100644
--- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx
+++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx
@@ -89,14 +89,18 @@ describe('', () => {
},
],
};
- catalogApi.getEntityByName.mockResolvedValue({
- apiVersion: 'v1',
- kind: 'System',
- metadata: {
- name: 'target-name',
- namespace: 'my-namespace',
- },
- spec: {},
+ catalogApi.getEntities.mockResolvedValue({
+ items: [
+ {
+ apiVersion: 'v1',
+ kind: 'System',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ },
+ ],
});
const { getByText } = await renderInTestApp(