diff --git a/.changeset/soft-roses-cover.md b/.changeset/soft-roses-cover.md new file mode 100644 index 0000000000..94cbe6d0bd --- /dev/null +++ b/.changeset/soft-roses-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Added `isEntityWith` condition helper for `EntitySwitch` case statements. diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 1c64fd3f68..124a612f26 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -480,7 +480,7 @@ function isKind(kind: string) { ``` The `@backstage/catalog` plugin provides a couple of built-in conditions, -`isKind`, `isComponentType`, and `isNamespace`. +`isKind`, `isComponentType`, `isResourceType`, `isEntityWith`, and `isNamespace`. In addition to the `EntitySwitch` component, the catalog plugin also exports a new `EntityLayout` component. It is a tweaked version and replacement for the diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index c678ce1d3d..ca91296913 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -355,6 +355,14 @@ export const EntityListContainer: (props: { // @public export function EntityOrphanWarning(): JSX.Element; +// @public (undocumented) +export interface EntityPredicates { + // (undocumented) + kind?: string | string[]; + // (undocumented) + type?: string | string[]; +} + // @public export function EntityProcessingErrorsPanel(): JSX.Element | null; @@ -433,6 +441,11 @@ export function isComponentType( types: string | string[], ): (entity: Entity) => boolean; +// @public +export function isEntityWith( + predicate: EntityPredicates, +): (entity: Entity) => boolean; + // @public export function isKind(kinds: string | string[]): (entity: Entity) => boolean; diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.test.ts b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts index 5d8c7c8898..66408aec18 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.test.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts @@ -20,6 +20,7 @@ import { isKind, isNamespace, isResourceType, + isEntityWith, } from './conditions'; const kubernetesClusterResource: Entity = { @@ -64,6 +65,13 @@ const notComponent: Entity = { spec: { type: 'service' }, }; +const missingSpecType: Entity = { + apiVersion: '', + kind: 'another-type', + metadata: { name: 'anEntity' }, + spec: {}, +}; + const apiKind: Entity = { apiVersion: '', kind: 'api', @@ -125,6 +133,25 @@ describe('isComponentType', () => { }); }); +describe('isEntityWith', () => { + it('allows for a kind-only check (empty type array)', () => { + const checkEntity = isEntityWith({ kind: 'api', type: [] }); + expect(checkEntity(apiKind)).toBeTruthy(); + }); + it('handles missing spec.type field', () => { + const checkEntity = isEntityWith({ kind: 'another-type', type: 'service' }); + expect(checkEntity(missingSpecType)).not.toBeTruthy(); + }); + it('allows a check against no criteria', () => { + const checkEntity = isEntityWith({}); + expect(checkEntity(apiKind)).toBeTruthy(); + }); + it('allows a check against empty criteria', () => { + const checkEntity = isEntityWith({ kind: [], type: [] }); + expect(checkEntity(apiKind)).toBeTruthy(); + }); +}); + describe('isKind', () => { it('should check for the intended kind', () => { const checkEntity = isKind('component'); diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index 222a76b0a9..f834804141 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -14,22 +14,26 @@ * limitations under the License. */ -import { - ComponentEntity, - ResourceEntity, - Entity, -} from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; -function strCmp(a: string | undefined, b: string | undefined): boolean { +/** @public */ +export interface EntityPredicates { + kind?: string | string[]; + type?: string | string[]; +} + +function strCmp(a: unknown, b: string | undefined): boolean { return Boolean( - a && a?.toLocaleLowerCase('en-US') === b?.toLocaleLowerCase('en-US'), + a && + typeof a === 'string' && + a?.toLocaleLowerCase('en-US') === b?.toLocaleLowerCase('en-US'), ); } -function strCmpAll(value: string | undefined, cmpValues: string | string[]) { +function strCmpAll(value: unknown, cmpValues: string | string[]) { return typeof cmpValues === 'string' ? strCmp(value, cmpValues) - : cmpValues.some(cmpVal => strCmp(value, cmpVal)); + : cmpValues.length === 0 || cmpValues.some(cmpVal => strCmp(value, cmpVal)); } /** @@ -37,7 +41,7 @@ function strCmpAll(value: string | undefined, cmpValues: string | string[]) { * @public */ export function isKind(kinds: string | string[]) { - return (entity: Entity) => strCmpAll(entity.kind, kinds); + return isEntityWith({ kind: kinds }); } /** @@ -45,13 +49,7 @@ export function isKind(kinds: string | string[]) { * @public */ export function isComponentType(types: string | string[]) { - return (entity: Entity) => { - if (!strCmp(entity.kind, 'component')) { - return false; - } - const componentEntity = entity as ComponentEntity; - return strCmpAll(componentEntity.spec.type, types); - }; + return isEntityWith({ kind: 'component', type: types }); } /** @@ -59,12 +57,25 @@ export function isComponentType(types: string | string[]) { * @public */ export function isResourceType(types: string | string[]) { + return isEntityWith({ kind: 'resource', type: types }); +} + +/** + * For use in EntitySwitch.Case. Matches if the entity is the specified kind and type (if present). + * @public + */ +export function isEntityWith(predicate: EntityPredicates) { return (entity: Entity) => { - if (!strCmp(entity.kind, 'resource')) { + if (predicate.kind && !strCmpAll(entity.kind, predicate.kind)) { return false; } - const resourceEntity = entity as ResourceEntity; - return strCmpAll(resourceEntity.spec.type, types); + + if (predicate.type && !strCmpAll(entity.spec?.type, predicate.type)) { + return false; + } + + // there's no type check, return true + return true; }; } diff --git a/plugins/catalog/src/components/EntitySwitch/index.ts b/plugins/catalog/src/components/EntitySwitch/index.ts index 0cb85ae6eb..8848f98866 100644 --- a/plugins/catalog/src/components/EntitySwitch/index.ts +++ b/plugins/catalog/src/components/EntitySwitch/index.ts @@ -16,9 +16,11 @@ export { EntitySwitch } from './EntitySwitch'; export type { EntitySwitchProps, EntitySwitchCaseProps } from './EntitySwitch'; +export type { EntityPredicates } from './conditions'; export { isKind, isNamespace, isComponentType, isResourceType, + isEntityWith, } from './conditions';