diff --git a/.changeset/silver-bikes-breathe.md b/.changeset/silver-bikes-breathe.md new file mode 100644 index 0000000000..63dabf7b2e --- /dev/null +++ b/.changeset/silver-bikes-breathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +allow entity switch to render all cases that match the condition diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 5720ee60da..d891ebb7c8 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -360,7 +360,7 @@ export function EntityProcessingErrorsPanel(): JSX.Element | null; // @public (undocumented) export const EntitySwitch: { - (props: EntitySwitchProps): JSX.Element | null; + (props: EntitySwitchProps): JSX.Element; Case: (_props: EntitySwitchCaseProps) => null; }; @@ -381,6 +381,8 @@ export interface EntitySwitchCaseProps { export interface EntitySwitchProps { // (undocumented) children: ReactNode; + // (undocumented) + renderMultipleMatches?: 'first' | 'all'; } // @public @deprecated (undocumented) diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index 164c7e60fd..8ee44041c8 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -131,6 +131,48 @@ describe('EntitySwitch', () => { expect(screen.getByText('B')).toBeInTheDocument(); }); + it('should render all elements that match the condition', () => { + const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; + + render( + + + + A

} /> + B

} /> + C

} /> + D

} /> +
+
+
, + ); + + expect(screen.getByText('A')).toBeInTheDocument(); + expect(screen.getByText('B')).toBeInTheDocument(); + expect(screen.queryByText('C')).not.toBeInTheDocument(); + expect(screen.queryByText('D')).not.toBeInTheDocument(); + }); + + it('should render default element if none of the cases match', () => { + const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; + + render( + + + + A

} /> + B

} /> + C

} /> +
+
+
, + ); + + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.getByText('C')).toBeInTheDocument(); + }); + it('should switch with async condition that is true', async () => { const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; @@ -150,6 +192,51 @@ describe('EntitySwitch', () => { expect(screen.queryByText('B')).not.toBeInTheDocument(); }); + it('should render all elements with async result as true', async () => { + const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; + + const shouldRender = () => Promise.resolve(true); + const shouldNotRender = () => Promise.resolve(false); + render( + + + + A

} /> + B

} /> + C

} /> + D

} /> +
+
+
, + ); + + await expect(screen.findByText('A')).resolves.toBeInTheDocument(); + await expect(screen.findByText('B')).resolves.toBeInTheDocument(); + expect(screen.queryByText('C')).not.toBeInTheDocument(); + expect(screen.queryByText('D')).not.toBeInTheDocument(); + }); + + it('should render default element if none of the async cases match', async () => { + const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; + + const shouldNotRender = () => Promise.resolve(false); + render( + + + + A

} /> + B

} /> + C

} /> +
+
+
, + ); + + await expect(screen.findByText('C')).resolves.toBeInTheDocument(); + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + }); + it('should switch with sync condition that is false', async () => { const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index 993e7daa80..6ad3c66ab3 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -49,7 +49,7 @@ interface EntitySwitchCase { } type SwitchCaseResult = { - if: boolean | Promise; + if?: boolean | Promise; children: JSX.Element; }; @@ -59,6 +59,7 @@ type SwitchCaseResult = { */ export interface EntitySwitchProps { children: ReactNode; + renderMultipleMatches?: 'first' | 'all'; } /** @public */ @@ -94,25 +95,45 @@ export const EntitySwitch = (props: EntitySwitchProps) => { } return [ { - if: condition?.(entity, { apis }) ?? true, + if: condition?.(entity, { apis }), children: elementsChildren, }, ]; }), [apis, entity, loading], ); + const hasAsyncCases = results.some( r => typeof r.if === 'object' && 'then' in r.if, ); if (hasAsyncCases) { - return ; + return ( + + ); } - return results.find(r => r.if)?.children ?? null; + if (props.renderMultipleMatches === 'all') { + const children = results.filter(r => r.if).map(r => r.children); + if (children.length === 0) { + return getDefaultChildren(results); + } + return <>{children}; + } + + return results.find(r => r.if)?.children ?? getDefaultChildren(results); }; -function AsyncEntitySwitch({ results }: { results: SwitchCaseResult[] }) { +function AsyncEntitySwitch({ + results, + renderMultipleMatches, +}: { + results: SwitchCaseResult[]; + renderMultipleMatches?: 'first' | 'all'; +}) { const { loading, value } = useAsync(async () => { const promises = results.map( async ({ if: condition, children: output }) => { @@ -123,11 +144,21 @@ function AsyncEntitySwitch({ results }: { results: SwitchCaseResult[] }) { } catch { /* ignored */ } - return null; }, ); - return (await Promise.all(promises)).find(Boolean) ?? null; + + if (renderMultipleMatches === 'all') { + const children = (await Promise.all(promises)).filter(Boolean); + if (children.length === 0) { + return getDefaultChildren(results); + } + return <>{children}; + } + + return ( + (await Promise.all(promises)).find(Boolean) ?? getDefaultChildren(results) + ); }, [results]); if (loading || !value) { @@ -137,4 +168,8 @@ function AsyncEntitySwitch({ results }: { results: SwitchCaseResult[] }) { return value; } +function getDefaultChildren(results: SwitchCaseResult[]) { + return results.filter(r => r.if === undefined)[0].children ?? null; +} + EntitySwitch.Case = EntitySwitchCaseComponent;