Skip to content

Commit 8834daf

Browse files
committed
Added for easier consumption
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
1 parent 9910c6b commit 8834daf

File tree

7 files changed

+119
-66
lines changed

7 files changed

+119
-66
lines changed

.changeset/proud-comics-love.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@backstage/plugin-catalog-react': minor
3+
'@backstage/plugin-catalog': minor
4+
---
5+
6+
Updated the presentation API to return a promise, in addition to the snapshot and observable that were there before. This makes it much easier to consume the API in a non-React context.

plugins/catalog-react/api-report.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ export type EntityRefLinksProps<
460460

461461
// @public
462462
export interface EntityRefPresentation {
463+
promise: Promise<EntityRefPresentationSnapshot>;
463464
snapshot: EntityRefPresentationSnapshot;
464465
update$?: Observable<EntityRefPresentationSnapshot>;
465466
}

plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ export interface EntityRefPresentation {
109109
* elsewhere.
110110
*/
111111
update$?: Observable<EntityRefPresentationSnapshot>;
112+
/**
113+
* A promise that resolves to a usable entity presentation.
114+
*/
115+
promise: Promise<EntityRefPresentationSnapshot>;
112116
}
113117

114118
/**

plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export function useEntityPresentation(
6161
const presentation = useMemo<EntityRefPresentation>(
6262
() => {
6363
if (!entityPresentationApi) {
64-
return { snapshot: defaultEntityPresentation(entityOrRef, context) };
64+
const fallback = defaultEntityPresentation(entityOrRef, context);
65+
return { snapshot: fallback, promise: Promise.resolve(fallback) };
6566
}
6667

6768
return entityPresentationApi.forEntity(

plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ describe('<EntityDisplayName />', () => {
7373
update$: new ObservableImpl(subscriber => {
7474
promise.then(value => subscriber.next(value));
7575
}),
76+
promise: Promise.resolve({
77+
entityRef: 'component:default/foo',
78+
primaryTitle: 'foo',
79+
}),
7680
} as EntityRefPresentation);
7781

7882
await renderInTestApp(

plugins/catalog/src/apis/EntityPresentationApi/DefaultEntityPresentationApi.test.ts

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ import {
2323
import { DefaultEntityPresentationApi } from './DefaultEntityPresentationApi';
2424

2525
describe('DefaultEntityPresentationApi', () => {
26-
it('works in local mode', () => {
26+
it('works in local mode', async () => {
2727
const api = DefaultEntityPresentationApi.createLocal();
2828

29-
expect(api.forEntity('component:default/test')).toEqual({
29+
let presentation = api.forEntity('component:default/test');
30+
expect(presentation).toEqual({
3031
snapshot: {
3132
entityRef: 'component:default/test',
3233
entity: undefined,
@@ -35,11 +36,14 @@ describe('DefaultEntityPresentationApi', () => {
3536
Icon: expect.anything(),
3637
},
3738
update$: undefined,
39+
promise: expect.any(Promise),
3840
});
41+
await expect(presentation.promise).resolves.toEqual(presentation.snapshot);
3942

40-
expect(
41-
api.forEntity('component:default/test', { defaultKind: 'Other' }),
42-
).toEqual({
43+
presentation = api.forEntity('component:default/test', {
44+
defaultKind: 'Other',
45+
});
46+
expect(presentation).toEqual({
4347
snapshot: {
4448
entityRef: 'component:default/test',
4549
entity: undefined,
@@ -48,13 +52,14 @@ describe('DefaultEntityPresentationApi', () => {
4852
Icon: expect.anything(),
4953
},
5054
update$: undefined,
55+
promise: expect.any(Promise),
5156
});
57+
await expect(presentation.promise).resolves.toEqual(presentation.snapshot);
5258

53-
expect(
54-
api.forEntity('component:default/test', {
55-
defaultNamespace: 'other',
56-
}),
57-
).toEqual({
59+
presentation = api.forEntity('component:default/test', {
60+
defaultNamespace: 'other',
61+
});
62+
expect(presentation).toEqual({
5863
snapshot: {
5964
entityRef: 'component:default/test',
6065
entity: undefined,
@@ -63,7 +68,9 @@ describe('DefaultEntityPresentationApi', () => {
6368
Icon: expect.anything(),
6469
},
6570
update$: undefined,
71+
promise: expect.any(Promise),
6672
});
73+
await expect(presentation.promise).resolves.toEqual(presentation.snapshot);
6774

6875
const entity: Entity = {
6976
apiVersion: 'backstage.io/v1alpha1',
@@ -77,15 +84,18 @@ describe('DefaultEntityPresentationApi', () => {
7784
},
7885
};
7986

80-
expect(api.forEntity(entity)).toEqual({
87+
presentation = api.forEntity(entity);
88+
expect(presentation).toEqual({
8189
snapshot: {
8290
entityRef: 'component:default/test',
8391
primaryTitle: 'test',
8492
secondaryTitle: 'component:default/test | service',
8593
Icon: expect.anything(),
8694
},
8795
update$: undefined,
96+
promise: expect.any(Promise),
8897
});
98+
await expect(presentation.promise).resolves.toEqual(presentation.snapshot);
8999
});
90100

91101
it('works in catalog mode', async () => {
@@ -114,34 +124,38 @@ describe('DefaultEntityPresentationApi', () => {
114124
});
115125

116126
// return simple presentation, call catalog, return full presentation
117-
await expect(
118-
consumePresentation(api.forEntity('component:default/test')),
119-
).resolves.toEqual([
127+
let presentation = api.forEntity('component:default/test');
128+
let expected: EntityRefPresentationSnapshot = {
129+
entityRef: 'component:default/test',
130+
primaryTitle: 'test',
131+
secondaryTitle: 'component:default/test | service',
132+
Icon: expect.anything(),
133+
};
134+
await expect(consumePresentation(presentation)).resolves.toEqual([
135+
// first the dummy snapshot
120136
{
121137
entityRef: 'component:default/test',
122138
primaryTitle: 'test',
123139
secondaryTitle: 'component:default/test',
124140
Icon: expect.anything(),
125141
},
126-
{
127-
entityRef: 'component:default/test',
128-
primaryTitle: 'test',
129-
secondaryTitle: 'component:default/test | service',
130-
Icon: expect.anything(),
131-
},
142+
expected,
132143
]);
144+
await expect(presentation.promise).resolves.toEqual(expected);
133145

134146
// use cached entity, immediately return full presentation
135-
await expect(
136-
consumePresentation(api.forEntity('component:default/test')),
137-
).resolves.toEqual([
138-
{
139-
entityRef: 'component:default/test',
140-
primaryTitle: 'test',
141-
secondaryTitle: 'component:default/test | service',
142-
Icon: expect.anything(),
143-
},
147+
presentation = api.forEntity('component:default/test');
148+
expected = {
149+
entityRef: 'component:default/test',
150+
primaryTitle: 'test',
151+
secondaryTitle: 'component:default/test | service',
152+
Icon: expect.anything(),
153+
};
154+
expect(presentation.snapshot).toEqual(expected);
155+
await expect(consumePresentation(presentation)).resolves.toEqual([
156+
expected,
144157
]);
158+
await expect(presentation.promise).resolves.toEqual(presentation.snapshot);
145159

146160
expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1);
147161
expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledWith(

plugins/catalog/src/apis/EntityPresentationApi/DefaultEntityPresentationApi.ts

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -260,47 +260,70 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
260260
};
261261
}
262262

263+
if (!needsLoad) {
264+
return {
265+
snapshot: initialSnapshot,
266+
promise: Promise.resolve(initialSnapshot),
267+
};
268+
}
269+
270+
const loadingPromise = Promise.resolve().then(() =>
271+
this.#loader?.load(entityRef),
272+
);
273+
263274
// And then the following snapshot
264-
const observable = !needsLoad
265-
? undefined
266-
: new ObservableImpl<EntityRefPresentationSnapshot>(subscriber => {
267-
let aborted = false;
268-
269-
Promise.resolve()
270-
.then(() => this.#loader?.load(entityRef))
271-
.then(newEntity => {
272-
if (
273-
!aborted &&
274-
newEntity &&
275-
newEntity.metadata.etag !== entity?.metadata.etag
276-
) {
277-
const updatedSnapshot = render({
278-
loading: false,
279-
entity: newEntity,
280-
});
281-
subscriber.next(updatedSnapshot);
282-
}
283-
})
284-
.catch(() => {
285-
// Intentionally ignored - we do not propagate errors to the
286-
// observable here. The presentation API should be error free and
287-
// always return SOMETHING that makes sense to render, and we have
288-
// already ensured above that the initial snapshot was that.
289-
})
290-
.finally(() => {
291-
if (!aborted) {
292-
subscriber.complete();
293-
}
294-
});
295-
296-
return () => {
297-
aborted = true;
298-
};
299-
});
275+
const observable = new ObservableImpl<EntityRefPresentationSnapshot>(
276+
subscriber => {
277+
let aborted = false;
278+
279+
loadingPromise
280+
.then(newEntity => {
281+
if (
282+
!aborted &&
283+
newEntity &&
284+
newEntity.metadata.etag !== entity?.metadata.etag
285+
) {
286+
const updatedSnapshot = render({
287+
loading: false,
288+
entity: newEntity,
289+
});
290+
subscriber.next(updatedSnapshot);
291+
}
292+
})
293+
.catch(() => {
294+
// Intentionally ignored - we do not propagate errors to the
295+
// observable here. The presentation API should be error free and
296+
// always return SOMETHING that makes sense to render, and we have
297+
// already ensured above that the initial snapshot was that.
298+
})
299+
.finally(() => {
300+
if (!aborted) {
301+
subscriber.complete();
302+
}
303+
});
304+
305+
return () => {
306+
aborted = true;
307+
};
308+
},
309+
);
310+
311+
const promise = loadingPromise
312+
.then(newEntity => {
313+
if (newEntity && newEntity.metadata.etag !== entity?.metadata.etag) {
314+
return render({
315+
loading: false,
316+
entity: newEntity,
317+
});
318+
}
319+
return initialSnapshot;
320+
})
321+
.catch(() => initialSnapshot);
300322

301323
return {
302324
snapshot: initialSnapshot,
303325
update$: observable,
326+
promise: promise,
304327
};
305328
}
306329

0 commit comments

Comments
 (0)