From fc6cab4eb487e04b7397744958bbceccc8423bf9 Mon Sep 17 00:00:00 2001 From: Brian Phillips <28457+brianphillips@users.noreply.github.com> Date: Tue, 14 Mar 2023 15:09:52 -0500 Subject: [PATCH 1/5] Add isEntityWith helper method to EntitySwitch conditions This will make it easier to conditionally show content for a given entity based on the `spec.type` field beyond just component entities (e.g. api, group and resource, all have a spec.type field) Signed-off-by: Brian Phillips <28457+brianphillips@users.noreply.github.com> --- .changeset/soft-roses-cover.md | 5 +++ docs/plugins/composability.md | 2 +- .../EntitySwitch/conditions.test.ts | 19 ++++++++ .../src/components/EntitySwitch/conditions.ts | 45 ++++++++++++------- 4 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 .changeset/soft-roses-cover.md diff --git a/.changeset/soft-roses-cover.md b/.changeset/soft-roses-cover.md new file mode 100644 index 0000000000..50a328729a --- /dev/null +++ b/.changeset/soft-roses-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Added isEntityWith condition helper 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/src/components/EntitySwitch/conditions.test.ts b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts index 5d8c7c8898..a0485c6a59 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,17 @@ 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(); + }); +}); + 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..0ffd9c0b01 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { - ComponentEntity, - ResourceEntity, - Entity, -} from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; + +export interface EntityPredicates { + kind: string | string[]; + type?: string | string[]; +} function strCmp(a: string | undefined, b: string | undefined): boolean { return Boolean( @@ -37,7 +38,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 +46,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,15 +54,33 @@ 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 (!strCmpAll(entity.kind, predicate.kind)) { return false; } - const resourceEntity = entity as ResourceEntity; - return strCmpAll(resourceEntity.spec.type, types); + + if (!predicate.type || (Array.isArray(predicate.type) && predicate.type.length === 0)) { + // there's no type check, return true + return true; + } else if (typeof entity.spec?.type !== 'string') { + // we'll reject any entity's with some non-string (including undefined/null) spec.type value + return false; + } + + return strCmpAll(entity.spec.type, predicate.type); }; } + /** * For use in EntitySwitch.Case. Matches if the entity is in a given namespace. * @public From da16cb69945255e28766179d36dabb9c2a65fc29 Mon Sep 17 00:00:00 2001 From: Brian Phillips <28457+brianphillips@users.noreply.github.com> Date: Wed, 15 Mar 2023 11:26:35 -0500 Subject: [PATCH 2/5] prettier... Signed-off-by: Brian Phillips <28457+brianphillips@users.noreply.github.com> --- .../src/components/EntitySwitch/conditions.test.ts | 4 ++-- plugins/catalog/src/components/EntitySwitch/conditions.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.test.ts b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts index a0485c6a59..76e9e7a7a8 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.test.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts @@ -135,11 +135,11 @@ describe('isComponentType', () => { describe('isEntityWith', () => { it('allows for a kind-only check (empty type array)', () => { - const checkEntity = isEntityWith({kind: 'api', type: []}); + const checkEntity = isEntityWith({ kind: 'api', type: [] }); expect(checkEntity(apiKind)).toBeTruthy(); }); it('handles missing spec.type field', () => { - const checkEntity = isEntityWith({kind: 'another-type', type: 'service'}); + const checkEntity = isEntityWith({ kind: 'another-type', type: 'service' }); expect(checkEntity(missingSpecType)).not.toBeTruthy(); }); }); diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index 0ffd9c0b01..5442a955ae 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -38,7 +38,7 @@ function strCmpAll(value: string | undefined, cmpValues: string | string[]) { * @public */ export function isKind(kinds: string | string[]) { - return isEntityWith({kind: kinds}); + return isEntityWith({ kind: kinds }); } /** @@ -68,7 +68,10 @@ export function isEntityWith(predicate: EntityPredicates) { return false; } - if (!predicate.type || (Array.isArray(predicate.type) && predicate.type.length === 0)) { + if ( + !predicate.type || + (Array.isArray(predicate.type) && predicate.type.length === 0) + ) { // there's no type check, return true return true; } else if (typeof entity.spec?.type !== 'string') { @@ -80,7 +83,6 @@ export function isEntityWith(predicate: EntityPredicates) { }; } - /** * For use in EntitySwitch.Case. Matches if the entity is in a given namespace. * @public From 642e39be2944115b8d48b48e2834b4989ece27e0 Mon Sep 17 00:00:00 2001 From: Brian Phillips <28457+brianphillips@users.noreply.github.com> Date: Fri, 17 Mar 2023 08:54:24 -0500 Subject: [PATCH 3/5] Apply code review feedback Allow more flexible comparisons where kind is not defined or the entity being compared against has some non-string value for `kind` or `spec.type`. Signed-off-by: Brian Phillips <28457+brianphillips@users.noreply.github.com> --- .changeset/soft-roses-cover.md | 2 +- .../EntitySwitch/conditions.test.ts | 8 ++++++ .../src/components/EntitySwitch/conditions.ts | 27 ++++++++----------- .../src/components/EntitySwitch/index.ts | 2 ++ 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.changeset/soft-roses-cover.md b/.changeset/soft-roses-cover.md index 50a328729a..94cbe6d0bd 100644 --- a/.changeset/soft-roses-cover.md +++ b/.changeset/soft-roses-cover.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog': minor --- -Added isEntityWith condition helper +Added `isEntityWith` condition helper for `EntitySwitch` case statements. diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.test.ts b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts index 76e9e7a7a8..66408aec18 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.test.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts @@ -142,6 +142,14 @@ describe('isEntityWith', () => { 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', () => { diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index 5442a955ae..02d7cf9bcf 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -17,20 +17,22 @@ import { Entity } from '@backstage/catalog-model'; export interface EntityPredicates { - kind: string | string[]; + kind?: string | string[]; type?: string | string[]; } -function strCmp(a: string | undefined, b: string | undefined): boolean { +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)); } /** @@ -58,28 +60,21 @@ export function isResourceType(types: string | string[]) { } /** - * * 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 (!strCmpAll(entity.kind, predicate.kind)) { + if (predicate.kind && !strCmpAll(entity.kind, predicate.kind)) { return false; } - if ( - !predicate.type || - (Array.isArray(predicate.type) && predicate.type.length === 0) - ) { - // there's no type check, return true - return true; - } else if (typeof entity.spec?.type !== 'string') { - // we'll reject any entity's with some non-string (including undefined/null) spec.type value + if (predicate.type && !strCmpAll(entity.spec?.type, predicate.type)) { return false; } - return strCmpAll(entity.spec.type, predicate.type); + // 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'; From a90f4d235e31a9e71e0c7c4279db4d35d1f242de Mon Sep 17 00:00:00 2001 From: Brian Phillips <28457+brianphillips@users.noreply.github.com> Date: Fri, 17 Mar 2023 09:34:42 -0500 Subject: [PATCH 4/5] Update api report for new exports Signed-off-by: Brian Phillips <28457+brianphillips@users.noreply.github.com> --- plugins/catalog/api-report.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index c678ce1d3d..52b2c958a7 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -355,6 +355,16 @@ export const EntityListContainer: (props: { // @public export function EntityOrphanWarning(): JSX.Element; +// Warning: (ae-missing-release-tag) "EntityPredicates" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface EntityPredicates { + // (undocumented) + kind?: string | string[]; + // (undocumented) + type?: string | string[]; +} + // @public export function EntityProcessingErrorsPanel(): JSX.Element | null; @@ -433,6 +443,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; From 33fdb2526a47e9525223c9645c6568988e9d9e45 Mon Sep 17 00:00:00 2001 From: Brian Phillips <28457+brianphillips@users.noreply.github.com> Date: Mon, 20 Mar 2023 16:28:46 -0500 Subject: [PATCH 5/5] mark EntityPredicates as @public Signed-off-by: Brian Phillips <28457+brianphillips@users.noreply.github.com> --- plugins/catalog/api-report.md | 2 -- plugins/catalog/src/components/EntitySwitch/conditions.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 52b2c958a7..ca91296913 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -355,8 +355,6 @@ export const EntityListContainer: (props: { // @public export function EntityOrphanWarning(): JSX.Element; -// Warning: (ae-missing-release-tag) "EntityPredicates" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface EntityPredicates { // (undocumented) diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index 02d7cf9bcf..f834804141 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; +/** @public */ export interface EntityPredicates { kind?: string | string[]; type?: string | string[];