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 <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2023-02-24 11:04:46 +02:00
parent 9c61bdaf40
commit 23cc40039c
4 changed files with 137 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': minor
---
allow entity switch to render all cases that match the condition
+3 -1
View File
@@ -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)
@@ -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(
<Wrapper>
<EntityProvider entity={entity}>
<EntitySwitch renderMultipleMatches="all">
<EntitySwitch.Case if={isKind('component')} children={<p>A</p>} />
<EntitySwitch.Case if={isKind('component')} children={<p>B</p>} />
<EntitySwitch.Case if={isKind('system')} children={<p>C</p>} />
<EntitySwitch.Case children={<p>D</p>} />
</EntitySwitch>
</EntityProvider>
</Wrapper>,
);
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(
<Wrapper>
<EntityProvider entity={entity}>
<EntitySwitch renderMultipleMatches="all">
<EntitySwitch.Case if={isKind('system')} children={<p>A</p>} />
<EntitySwitch.Case if={isKind('system')} children={<p>B</p>} />
<EntitySwitch.Case children={<p>C</p>} />
</EntitySwitch>
</EntityProvider>
</Wrapper>,
);
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(
<Wrapper>
<EntityProvider entity={entity}>
<EntitySwitch renderMultipleMatches="all">
<EntitySwitch.Case if={shouldRender} children={<p>A</p>} />
<EntitySwitch.Case if={shouldRender} children={<p>B</p>} />
<EntitySwitch.Case if={shouldNotRender} children={<p>C</p>} />
<EntitySwitch.Case children={<p>D</p>} />
</EntitySwitch>
</EntityProvider>
</Wrapper>,
);
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(
<Wrapper>
<EntityProvider entity={entity}>
<EntitySwitch renderMultipleMatches="all">
<EntitySwitch.Case if={shouldNotRender} children={<p>A</p>} />
<EntitySwitch.Case if={shouldNotRender} children={<p>B</p>} />
<EntitySwitch.Case children={<p>C</p>} />
</EntitySwitch>
</EntityProvider>
</Wrapper>,
);
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;
@@ -49,7 +49,7 @@ interface EntitySwitchCase {
}
type SwitchCaseResult = {
if: boolean | Promise<boolean>;
if?: boolean | Promise<boolean>;
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 <AsyncEntitySwitch results={results} />;
return (
<AsyncEntitySwitch
results={results}
renderMultipleMatches={props.renderMultipleMatches}
/>
);
}
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;