From 23cc40039c06564c4e5524b7eefd961449d87a80 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Fri, 24 Feb 2023 11:04:46 +0200 Subject: [PATCH] feat: allow entity switch to render all cases that match this is required for example in the CI/CD page where multiple different kinds of integrations might need rendering (some might have for example jenkins, AWS codebuild and AWS codepipeline for single component). by default, the EntitSwitch will behave like it used to so it's not a breaking change. Signed-off-by: Heikki Hellgren --- .changeset/silver-bikes-breathe.md | 5 ++ plugins/catalog/api-report.md | 4 +- .../EntitySwitch/EntitySwitch.test.tsx | 87 +++++++++++++++++++ .../components/EntitySwitch/EntitySwitch.tsx | 49 +++++++++-- 4 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 .changeset/silver-bikes-breathe.md 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 ee0ca180f4..0524265c2d 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -352,7 +352,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; }; @@ -373,6 +373,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;