diff --git a/.changeset/mean-insects-hope.md b/.changeset/mean-insects-hope.md
new file mode 100644
index 0000000000..0a71a9c1ee
--- /dev/null
+++ b/.changeset/mean-insects-hope.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+Make `useRelatedEntities` use `getEntitiesByRefs` under the hood
diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
index ea51d3989d..d664ce78e8 100644
--- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
@@ -32,12 +32,7 @@ describe('', () => {
getApiDefinitionWidget: jest.fn(),
} as any;
const catalogApi: jest.Mocked = {
- getLocationById: jest.fn(),
- getEntityByName: jest.fn(),
- getEntities: jest.fn(),
- addLocation: jest.fn(),
- getLocationByRef: jest.fn(),
- removeEntityByUid: jest.fn(),
+ getEntitiesByRefs: jest.fn(),
} as any;
let Wrapper: React.ComponentType>;
@@ -99,7 +94,7 @@ describe('', () => {
},
],
};
- catalogApi.getEntities.mockResolvedValue({
+ catalogApi.getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx
index 6ab55c5c66..169311d0c8 100644
--- a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx
+++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx
@@ -32,12 +32,7 @@ describe('', () => {
getApiDefinitionWidget: jest.fn(),
} as any;
const catalogApi: jest.Mocked = {
- getLocationById: jest.fn(),
- getEntityByName: jest.fn(),
- getEntities: jest.fn(),
- addLocation: jest.fn(),
- getLocationByRef: jest.fn(),
- removeEntityByUid: jest.fn(),
+ getEntitiesByRefs: jest.fn(),
} as any;
let Wrapper: React.ComponentType>;
@@ -99,7 +94,7 @@ describe('', () => {
},
],
};
- catalogApi.getEntities.mockResolvedValue({
+ catalogApi.getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
index 5f69e90f9f..3dbc4edd63 100644
--- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
@@ -32,12 +32,7 @@ describe('', () => {
getApiDefinitionWidget: jest.fn(),
} as any;
const catalogApi: jest.Mocked = {
- getLocationById: jest.fn(),
- getEntityByName: jest.fn(),
- getEntities: jest.fn(),
- addLocation: jest.fn(),
- getLocationByRef: jest.fn(),
- removeEntityByUid: jest.fn(),
+ getEntitiesByRefs: jest.fn(),
} as any;
let Wrapper: React.ComponentType>;
@@ -99,7 +94,7 @@ describe('', () => {
},
],
};
- catalogApi.getEntities.mockResolvedValue({
+ catalogApi.getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
index f7a18cf671..20a60562b0 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
+++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
@@ -28,12 +28,7 @@ import { ConsumingComponentsCard } from './ConsumingComponentsCard';
describe('', () => {
const catalogApi: jest.Mocked = {
- getLocationById: jest.fn(),
- getEntityByName: jest.fn(),
- getEntities: jest.fn(),
- addLocation: jest.fn(),
- getLocationByRef: jest.fn(),
- removeEntityByUid: jest.fn(),
+ getEntitiesByRefs: jest.fn(),
} as any;
let Wrapper: React.ComponentType>;
@@ -102,7 +97,7 @@ describe('', () => {
},
],
};
- catalogApi.getEntities.mockResolvedValue({
+ catalogApi.getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
index 8b003bf2e2..0801d7103a 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
+++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
@@ -28,12 +28,7 @@ import { ProvidingComponentsCard } from './ProvidingComponentsCard';
describe('', () => {
const catalogApi: jest.Mocked = {
- getLocationById: jest.fn(),
- getEntityByName: jest.fn(),
- getEntities: jest.fn(),
- addLocation: jest.fn(),
- getLocationByRef: jest.fn(),
- removeEntityByUid: jest.fn(),
+ getEntitiesByRefs: jest.fn(),
} as any;
let Wrapper: React.ComponentType>;
@@ -102,7 +97,7 @@ describe('', () => {
},
],
};
- catalogApi.getEntities.mockResolvedValue({
+ catalogApi.getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md
index 90bddecd71..2fab514de1 100644
--- a/plugins/catalog-react/api-report.md
+++ b/plugins/catalog-react/api-report.md
@@ -555,7 +555,7 @@ export function useEntityTypeFilter(): {
setSelectedTypes: (types: string[]) => void;
};
-// @public (undocumented)
+// @public
export function useRelatedEntities(
entity: Entity,
relationFilter: {
diff --git a/plugins/catalog-react/src/hooks/useRelatedEntities.test.tsx b/plugins/catalog-react/src/hooks/useRelatedEntities.test.tsx
new file mode 100644
index 0000000000..0d415274e0
--- /dev/null
+++ b/plugins/catalog-react/src/hooks/useRelatedEntities.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 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 { TestApiProvider } from '@backstage/test-utils';
+import { WrapperComponent, renderHook } from '@testing-library/react-hooks';
+import React from 'react';
+import { catalogApiRef } from '../api';
+import { useRelatedEntities } from './useRelatedEntities';
+
+describe('useRelatedEntities', () => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ const entity: Entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: { name: 'test' },
+ relations: [
+ {
+ type: 'ownedBy',
+ targetRef: 'group:default/the-owners-1',
+ },
+ {
+ type: 'ownedBy',
+ targetRef: 'group:default/the-owners-2',
+ },
+ {
+ type: 'partOf',
+ targetRef: 'component:default/larger-thing',
+ },
+ ],
+ };
+
+ const catalogApi = {
+ getEntitiesByRefs: jest.fn(),
+ };
+
+ const wrapper: WrapperComponent<{}> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ it('filters and requests entities', async () => {
+ catalogApi.getEntitiesByRefs.mockResolvedValueOnce({
+ items: [entity, null], // one of them doesn't exist
+ });
+
+ const rendered = renderHook(
+ () => useRelatedEntities(entity, { type: 'ownedby', kind: 'grOUP' }),
+ { wrapper },
+ );
+
+ expect(rendered.result.current).toEqual({ loading: true });
+
+ await rendered.waitForValueToChange(() => rendered.result.current.loading);
+
+ expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledWith({
+ entityRefs: ['group:default/the-owners-1', 'group:default/the-owners-2'],
+ });
+
+ expect(rendered.result.current).toEqual({
+ loading: false,
+ entities: [entity], // filtered out the null
+ });
+ });
+});
diff --git a/plugins/catalog-react/src/hooks/useRelatedEntities.ts b/plugins/catalog-react/src/hooks/useRelatedEntities.ts
index 4cf5690ac3..7ec0a13d53 100644
--- a/plugins/catalog-react/src/hooks/useRelatedEntities.ts
+++ b/plugins/catalog-react/src/hooks/useRelatedEntities.ts
@@ -13,15 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { Entity, parseEntityRef } from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
-import { chunk, groupBy } from 'lodash';
import useAsync from 'react-use/lib/useAsync';
import { catalogApiRef } from '../api';
-const BATCH_SIZE = 20;
-
-/** @public */
+/**
+ * Fetches all entities that appear in the entity's relations, optionally
+ * filtered by relation type and kind.
+ *
+ * @public
+ */
export function useRelatedEntities(
entity: Entity,
relationFilter: { type?: string; kind?: string },
@@ -32,70 +35,30 @@ export function useRelatedEntities(
} {
const filterByTypeLower = relationFilter?.type?.toLocaleLowerCase('en-US');
const filterByKindLower = relationFilter?.kind?.toLocaleLowerCase('en-US');
-
const catalogApi = useApi(catalogApiRef);
+
const {
loading,
value: entities,
error,
} = useAsync(async () => {
- const relations = entity.relations
- ?.map(r => ({ type: r.type, target: parseEntityRef(r.targetRef) }))
- .filter(
- r =>
- (!filterByTypeLower ||
- r.type.toLocaleLowerCase('en-US') === filterByTypeLower) &&
- (!filterByKindLower || r.target.kind === filterByKindLower),
- );
+ const relations = entity.relations?.filter(
+ r =>
+ (!filterByTypeLower ||
+ r.type.toLocaleLowerCase('en-US') === filterByTypeLower) &&
+ (!filterByKindLower ||
+ parseEntityRef(r.targetRef).kind === filterByKindLower),
+ );
- if (!relations) {
+ if (!relations?.length) {
return [];
}
- // 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 = Object.values(
- groupBy(relations, ({ target }) => {
- return `${target.kind}:${target.namespace}`.toLocaleLowerCase('en-US');
- }),
- );
+ const { items } = await catalogApi.getEntitiesByRefs({
+ entityRefs: relations.map(r => r.targetRef),
+ });
- // 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);
+ return items.filter((x): x is Entity => Boolean(x));
}, [entity, filterByTypeLower, filterByKindLower]);
return {
diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx
index e5900d75e0..0ebda6e64d 100644
--- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx
+++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx
@@ -27,12 +27,14 @@ import React from 'react';
import { DependencyOfComponentsCard } from './DependencyOfComponentsCard';
describe('', () => {
- const getEntities: jest.MockedFunction = jest.fn();
+ const getEntitiesByRefs: jest.MockedFunction<
+ CatalogApi['getEntitiesByRefs']
+ > = jest.fn();
let Wrapper: React.ComponentType>;
beforeEach(() => {
Wrapper = ({ children }: { children?: React.ReactNode }) => (
-
+
{children}
);
@@ -85,7 +87,7 @@ describe('', () => {
},
],
};
- getEntities.mockResolvedValue({
+ getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx
index 9255ee4f36..ee8931143f 100644
--- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx
+++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx
@@ -27,12 +27,14 @@ import React from 'react';
import { DependsOnComponentsCard } from './DependsOnComponentsCard';
describe('', () => {
- const getEntities: jest.MockedFunction = jest.fn();
+ const getEntitiesByRefs: jest.MockedFunction<
+ CatalogApi['getEntitiesByRefs']
+ > = jest.fn();
let Wrapper: React.ComponentType>;
beforeEach(() => {
Wrapper = ({ children }: { children?: React.ReactNode }) => (
-
+
{children}
);
@@ -85,7 +87,7 @@ describe('', () => {
},
],
};
- getEntities.mockResolvedValue({
+ getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx
index 3b0d7dcff3..0e760f9155 100644
--- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx
+++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx
@@ -27,12 +27,14 @@ import React from 'react';
import { DependsOnResourcesCard } from './DependsOnResourcesCard';
describe('', () => {
- const getEntities: jest.MockedFunction = jest.fn();
+ const getEntitiesByRefs: jest.MockedFunction<
+ CatalogApi['getEntitiesByRefs']
+ > = jest.fn();
let Wrapper: React.ComponentType>;
beforeEach(() => {
Wrapper = ({ children }: { children?: React.ReactNode }) => (
-
+
{children}
);
@@ -85,7 +87,7 @@ describe('', () => {
},
],
};
- getEntities.mockResolvedValue({
+ getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx
index 3651421797..67c3f6b650 100644
--- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx
+++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx
@@ -27,12 +27,14 @@ import React from 'react';
import { HasComponentsCard } from './HasComponentsCard';
describe('', () => {
- const getEntities: jest.MockedFunction = jest.fn();
+ const getEntitiesByRefs: jest.MockedFunction<
+ CatalogApi['getEntitiesByRefs']
+ > = jest.fn();
let Wrapper: React.ComponentType>;
beforeEach(() => {
Wrapper = ({ children }: { children?: React.ReactNode }) => (
-
+
{children}
);
@@ -85,7 +87,7 @@ describe('', () => {
},
],
};
- getEntities.mockResolvedValue({
+ getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx
index 4bf6524a9c..d78d307c96 100644
--- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx
+++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx
@@ -27,12 +27,14 @@ import React from 'react';
import { HasResourcesCard } from './HasResourcesCard';
describe('', () => {
- const getEntities: jest.MockedFunction = jest.fn();
+ const getEntitiesByRefs: jest.MockedFunction<
+ CatalogApi['getEntitiesByRefs']
+ > = jest.fn();
let Wrapper: React.ComponentType>;
beforeEach(() => {
Wrapper = ({ children }: { children?: React.ReactNode }) => (
-
+
{children}
);
@@ -80,7 +82,7 @@ describe('', () => {
},
],
};
- getEntities.mockResolvedValue({
+ getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx
index 97e69e93d1..713313b7c6 100644
--- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx
+++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx
@@ -27,12 +27,14 @@ import React from 'react';
import { HasSubcomponentsCard } from './HasSubcomponentsCard';
describe('', () => {
- const getEntities: jest.MockedFunction = jest.fn();
+ const getEntitiesByRefs: jest.MockedFunction<
+ CatalogApi['getEntitiesByRefs']
+ > = jest.fn();
let Wrapper: React.ComponentType>;
beforeEach(() => {
Wrapper = ({ children }: { children?: React.ReactNode }) => (
-
+
{children}
);
@@ -85,7 +87,7 @@ describe('', () => {
},
],
};
- getEntities.mockResolvedValue({
+ getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx
index 224e0c6703..9844708f85 100644
--- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx
+++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx
@@ -27,12 +27,14 @@ import React from 'react';
import { HasSystemsCard } from './HasSystemsCard';
describe('', () => {
- const getEntities: jest.MockedFunction = jest.fn();
+ const getEntitiesByRefs: jest.MockedFunction<
+ CatalogApi['getEntitiesByRefs']
+ > = jest.fn();
let Wrapper: React.ComponentType>;
beforeEach(() => {
Wrapper = ({ children }: { children?: React.ReactNode }) => (
-
+
{children}
);
@@ -85,7 +87,7 @@ describe('', () => {
},
],
};
- getEntities.mockResolvedValue({
+ getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',