diff --git a/.changeset/gentle-onions-occur.md b/.changeset/gentle-onions-occur.md new file mode 100644 index 0000000000..29653e39c2 --- /dev/null +++ b/.changeset/gentle-onions-occur.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-org': patch +--- + +Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. diff --git a/.changeset/red-chicken-juggle.md b/.changeset/red-chicken-juggle.md new file mode 100644 index 0000000000..0eb2df8b65 --- /dev/null +++ b/.changeset/red-chicken-juggle.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING ALPHA**: All of the predicate types and functions have been moved to the `@backstage/filter-predicates` package. + +When moving into the more general package, they were renamed as follows: + +- `EntityPredicate` -> `FilterPredicate` +- `EntityPredicateExpression` -> `FilterPredicateExpression` +- `EntityPredicatePrimitive` -> `FilterPredicatePrimitive` +- `entityPredicateToFilterFunction` -> `filterPredicateToFilterFunction` +- `EntityPredicateValue` -> `FilterPredicateValue` diff --git a/.changeset/wise-rabbits-double.md b/.changeset/wise-rabbits-double.md new file mode 100644 index 0000000000..6164c6e95c --- /dev/null +++ b/.changeset/wise-rabbits-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/filter-predicates': minor +--- + +Introduced package, basically as the extracted predicate types from `@backstage/plugin-catalog-react/alpha` diff --git a/.gitignore b/.gitignore index aaf05b51cd..dec469903e 100644 --- a/.gitignore +++ b/.gitignore @@ -186,4 +186,7 @@ docs.json tsconfig.typedoc.tmp.json # Storybook -dist-storybook/ \ No newline at end of file +dist-storybook/ + +# Personal allow patterns etc +.claude/settings.local.json diff --git a/packages/filter-predicates/.eslintrc.js b/packages/filter-predicates/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/filter-predicates/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/filter-predicates/README.md b/packages/filter-predicates/README.md new file mode 100644 index 0000000000..e8f70fe5aa --- /dev/null +++ b/packages/filter-predicates/README.md @@ -0,0 +1,18 @@ +# @backstage/filter-predicates + +Contains types and implementations related to the concept of +[filter predicate expressions](https://backstage.io/docs/features/software-catalog/catalog-customization#entity-predicate-queries). + +These allow you to uniformly express filters, including logical operators and +advanced matchers, for filtering through structured data. + +Example: + +```json +{ + "filter": { + "kind": "Component", + "spec.type": { "$in": ["service", "website"] } + } +} +``` diff --git a/packages/filter-predicates/catalog-info.yaml b/packages/filter-predicates/catalog-info.yaml new file mode 100644 index 0000000000..63be7ba015 --- /dev/null +++ b/packages/filter-predicates/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-filter-predicates + title: '@backstage/filter-predicates' + description: A library for expressing filter predicates and evaluating them against values +spec: + lifecycle: experimental + type: backstage-common-library + owner: framework-maintainers diff --git a/packages/filter-predicates/package.json b/packages/filter-predicates/package.json new file mode 100644 index 0000000000..de803f713e --- /dev/null +++ b/packages/filter-predicates/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/filter-predicates", + "version": "0.0.0", + "description": "A library for expressing filter predicates and evaluating them against values", + "backstage": { + "role": "common-library" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "keywords": [ + "backstage" + ], + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/filter-predicates" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/types": "workspace:^", + "zod": "^3.25.76", + "zod-validation-error": "^4.0.2" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/filter-predicates/report.api.md b/packages/filter-predicates/report.api.md new file mode 100644 index 0000000000..9b729c3d60 --- /dev/null +++ b/packages/filter-predicates/report.api.md @@ -0,0 +1,107 @@ +## API Report File for "@backstage/filter-predicates" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; +import * as zodV3 from 'zod/v3'; + +// @public +export function createZodV3FilterPredicateSchema( + z: typeof zodV3.z, +): zodV3.ZodType; + +// @public +export function evaluateFilterPredicate( + predicate: FilterPredicate, + value: unknown, +): boolean; + +// @public +export type FilterPredicate = + | FilterPredicateExpression + | FilterPredicatePrimitive + | { + $all: FilterPredicate[]; + } + | { + $any: FilterPredicate[]; + } + | { + $not: FilterPredicate; + } + | UnknownFilterPredicateOperator; + +// @public +export type FilterPredicateExpression = { + [KPath in string]: FilterPredicateValue; +} & { + [KPath in `$${string}`]: never; +}; + +// @public +export type FilterPredicatePrimitive = string | number | boolean; + +// @public +export function filterPredicateToFilterFunction( + predicate: FilterPredicate, +): (value: T) => boolean; + +// @public +export type FilterPredicateValue = + | FilterPredicatePrimitive + | { + $exists: boolean; + } + | { + $in: FilterPredicatePrimitive[]; + } + | { + $contains: FilterPredicate; + } + | { + $startsWith: string; + } + | UnknownFilterPredicateValueMatcher; + +// @public +export function getJsonValueAtPath( + value: JsonValue | undefined, + path: string, +): JsonValue | undefined; + +// @public +export function parseFilterPredicate(value: unknown): FilterPredicate; + +// @public +export function readFilterPredicateFromConfig( + config: Config, + options?: ReadFilterPredicateFromConfigOptions, +): FilterPredicate; + +// @public +export interface ReadFilterPredicateFromConfigOptions { + key?: string; +} + +// @public +export function readOptionalFilterPredicateFromConfig( + config: Config, + options?: ReadFilterPredicateFromConfigOptions, +): FilterPredicate | undefined; + +// @public +export type UnknownFilterPredicateOperator = { + [KOperator in `$${string}`]: JsonValue; +} & { + [KOperator in '$all' | '$any' | '$not']: never; +}; + +// @public +export type UnknownFilterPredicateValueMatcher = { + [KMatcher in `$${string}`]: JsonValue; +} & { + [KMatcher in '$exists' | '$in' | '$contains' | '$startsWith']: never; +}; +``` diff --git a/plugins/catalog-react/src/alpha/predicates/index.ts b/packages/filter-predicates/src/index.ts similarity index 73% rename from plugins/catalog-react/src/alpha/predicates/index.ts rename to packages/filter-predicates/src/index.ts index 71be7403de..414b4127a5 100644 --- a/plugins/catalog-react/src/alpha/predicates/index.ts +++ b/packages/filter-predicates/src/index.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -export type { - EntityPredicate, - EntityPredicateExpression, - EntityPredicatePrimitive, - EntityPredicateValue, -} from './types'; -export { entityPredicateToFilterFunction } from './entityPredicateToFilterFunction'; +/** + * Contains types and implementations related to the concept of filter predicate expressions. + * + * @packageDocumentation + */ + +export * from './predicates'; diff --git a/packages/filter-predicates/src/predicates/config.test.ts b/packages/filter-predicates/src/predicates/config.test.ts new file mode 100644 index 0000000000..494e707f40 --- /dev/null +++ b/packages/filter-predicates/src/predicates/config.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { + readFilterPredicateFromConfig, + readOptionalFilterPredicateFromConfig, +} from './config'; + +describe('readFilterPredicateFromConfig', () => { + it('should read a filter predicate from config', () => { + const config = new ConfigReader({ + predicate: { kind: 'component', 'spec.type': 'service' }, + }); + + const result = readFilterPredicateFromConfig(config, { key: 'predicate' }); + + expect(result).toEqual({ kind: 'component', 'spec.type': 'service' }); + }); + + it('should read a filter predicate from the root config', () => { + const config = new ConfigReader({ + kind: 'component', + 'spec.type': 'service', + }); + + const result = readFilterPredicateFromConfig(config); + + expect(result).toEqual({ kind: 'component', 'spec.type': 'service' }); + }); + + it('should throw when filter predicate is missing', () => { + const config = new ConfigReader({}); + + expect(() => + readFilterPredicateFromConfig(config, { key: 'predicate' }), + ).toThrow(/predicate/); + }); + + it('should throw when filter predicate is invalid', () => { + const config = new ConfigReader({ + predicate: { kind: { $invalid: 'foo' } }, + }); + + expect(() => + readFilterPredicateFromConfig(config, { key: 'predicate' }), + ).toThrow(/Could not read filter predicate from config at 'predicate':/); + }); +}); + +describe('readOptionalFilterPredicateFromConfig', () => { + it('should read a filter predicate from config', () => { + const config = new ConfigReader({ + predicate: { kind: 'component' }, + }); + + const result = readOptionalFilterPredicateFromConfig(config, { + key: 'predicate', + }); + + expect(result).toEqual({ kind: 'component' }); + }); + + it('should return undefined when filter predicate is missing', () => { + const config = new ConfigReader({}); + + const result = readOptionalFilterPredicateFromConfig(config, { + key: 'predicate', + }); + + expect(result).toBeUndefined(); + }); + + it('should throw when filter predicate is invalid', () => { + const config = new ConfigReader({ + predicate: { kind: { $invalid: 'foo' } }, + }); + + expect(() => + readOptionalFilterPredicateFromConfig(config, { key: 'predicate' }), + ).toThrow(/Could not read filter predicate from config at 'predicate':/); + }); + + it('should read complex filter predicates', () => { + const config = new ConfigReader({ + filter: { + $any: [{ kind: 'component', 'spec.type': 'service' }, { kind: 'api' }], + }, + }); + + const result = readOptionalFilterPredicateFromConfig(config, { + key: 'filter', + }); + + expect(result).toEqual({ + $any: [{ kind: 'component', 'spec.type': 'service' }, { kind: 'api' }], + }); + }); +}); diff --git a/packages/filter-predicates/src/predicates/config.ts b/packages/filter-predicates/src/predicates/config.ts new file mode 100644 index 0000000000..12f0620523 --- /dev/null +++ b/packages/filter-predicates/src/predicates/config.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { InputError, stringifyError } from '@backstage/errors'; +import { parseFilterPredicate } from './schema'; +import { FilterPredicate } from './types'; + +/** + * Options for {@link readFilterPredicateFromConfig} and {@link readOptionalFilterPredicateFromConfig}. + * + * @public + */ +export interface ReadFilterPredicateFromConfigOptions { + /** + * The key to read from the config. If not provided, the entire config is used. + */ + key?: string; +} + +/** + * Read a filter predicate expression from a config object. + * + * @public + */ +export function readFilterPredicateFromConfig( + config: Config, + options?: ReadFilterPredicateFromConfigOptions, +): FilterPredicate { + const key = options?.key; + const value = key ? config.get(key) : config.get(); + + try { + return parseFilterPredicate(value); + } catch (error) { + const where = key ? ` at '${key}'` : ''; + throw new InputError( + `Could not read filter predicate from config${where}: ${stringifyError( + error, + )}`, + ); + } +} + +/** + * Read an optional filter predicate expression from a config object. + * + * @public + */ +export function readOptionalFilterPredicateFromConfig( + config: Config, + options?: ReadFilterPredicateFromConfigOptions, +): FilterPredicate | undefined { + const key = options?.key; + const value = key ? config.getOptional(key) : config.getOptional(); + + if (value === undefined) { + return undefined; + } + + return readFilterPredicateFromConfig(config, options); +} diff --git a/plugins/catalog-react/src/alpha/predicates/entityPredicateToFilterFunction.test.ts b/packages/filter-predicates/src/predicates/evaluate.test.ts similarity index 79% rename from plugins/catalog-react/src/alpha/predicates/entityPredicateToFilterFunction.test.ts rename to packages/filter-predicates/src/predicates/evaluate.test.ts index 152230de7c..f28c1c9453 100644 --- a/plugins/catalog-react/src/alpha/predicates/entityPredicateToFilterFunction.test.ts +++ b/packages/filter-predicates/src/predicates/evaluate.test.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -import { entityPredicateToFilterFunction } from './entityPredicateToFilterFunction'; -import { EntityPredicate } from './types'; +import { + evaluateFilterPredicate, + filterPredicateToFilterFunction, +} from './evaluate'; +import { FilterPredicate } from './types'; -describe('entityPredicateToFilterFunction', () => { +describe('evaluate', () => { const entities = [ { apiVersion: 'backstage.io/v1alpha1', @@ -129,7 +132,7 @@ describe('entityPredicateToFilterFunction', () => { }, ]; - it.each([ + describe.each([ ['s', { kind: 'component', 'spec.type': 'service' }], ['s', { 'metadata.tags': { $contains: 'java' } }], [ @@ -186,7 +189,7 @@ describe('entityPredicateToFilterFunction', () => { metadata: { $contains: { name: 'a' } }, }, ], - ['', { $unknown: 'ignored' } as unknown as EntityPredicate], + ['', { $unknown: 'ignored' } as unknown as FilterPredicate], [ 's,w', { kind: 'component', 'spec.type': { $in: ['service', 'website'] } }, @@ -234,12 +237,33 @@ describe('entityPredicateToFilterFunction', () => { 'metadata.annotations.github.com/repo': { $exists: true }, }, ], + ['a', { 'spec.type': { $startsWith: 'g' } }], ])('filter entry %#', (expected, filter) => { - const filtered = entities.filter(entity => - entityPredicateToFilterFunction(filter)(entity), - ); - expect(filtered.map(e => e.metadata.name).sort()).toEqual( - expected.split(',').filter(Boolean).sort(), - ); + it('filterPredicateToFilterFunction', () => { + const filtered = entities.filter(entity => + filterPredicateToFilterFunction(filter)(entity), + ); + expect(filtered.map(e => e.metadata.name).sort()).toEqual( + expected.split(',').filter(Boolean).sort(), + ); + }); + + it('evaluateFilterPredicate', () => { + const filtered = entities.filter(entity => + evaluateFilterPredicate(filter, entity), + ); + expect(filtered.map(e => e.metadata.name).sort()).toEqual( + expected.split(',').filter(Boolean).sort(), + ); + }); + }); + + it('handles unknown filter predicate operators and matchers', () => { + const operator = { $unknown: 'foo' } as unknown as FilterPredicate; + const value = { kind: { $unknown: 'foo' } } as unknown as FilterPredicate; + expect(evaluateFilterPredicate(operator, entities[0])).toBe(false); + expect(evaluateFilterPredicate(value, entities[0])).toBe(false); + expect(filterPredicateToFilterFunction(operator)(entities[0])).toBe(false); + expect(filterPredicateToFilterFunction(value)(entities[0])).toBe(false); }); }); diff --git a/plugins/catalog-react/src/alpha/predicates/entityPredicateToFilterFunction.ts b/packages/filter-predicates/src/predicates/evaluate.ts similarity index 53% rename from plugins/catalog-react/src/alpha/predicates/entityPredicateToFilterFunction.ts rename to packages/filter-predicates/src/predicates/evaluate.ts index f223911992..170bee41f3 100644 --- a/plugins/catalog-react/src/alpha/predicates/entityPredicateToFilterFunction.ts +++ b/packages/filter-predicates/src/predicates/evaluate.ts @@ -15,51 +15,48 @@ */ import { JsonValue } from '@backstage/types'; -import { EntityPredicate, EntityPredicateValue } from './types'; -import { valueAtPath } from './valueAtPath'; +import { FilterPredicate, FilterPredicateValue } from './types'; +import { getJsonValueAtPath } from './getJsonValueAtPath'; /** - * Convert an entity predicate to a filter function that can be used to filter entities. - * @alpha - */ -export function entityPredicateToFilterFunction( - entityPredicate: EntityPredicate, -): (value: T) => boolean { - return value => evaluateEntityPredicate(entityPredicate, value); -} - -/** - * Evaluate a entity predicate against a value, typically an entity. + * Evaluate a filter predicate against a value. * - * @internal + * @public */ -function evaluateEntityPredicate( - filter: EntityPredicate, - value: JsonValue, +export function evaluateFilterPredicate( + predicate: FilterPredicate, + value: unknown, ): boolean { - if (typeof filter !== 'object' || filter === null || Array.isArray(filter)) { - return valuesAreEqual(value, filter); + if ( + typeof predicate !== 'object' || + predicate === null || + Array.isArray(predicate) + ) { + return valuesAreEqual(value, predicate); } - if ('$all' in filter) { - return filter.$all.every(f => evaluateEntityPredicate(f, value)); + if ('$all' in predicate) { + return predicate.$all.every(f => evaluateFilterPredicate(f, value)); } - if ('$any' in filter) { - return filter.$any.some(f => evaluateEntityPredicate(f, value)); + if ('$any' in predicate) { + return predicate.$any.some(f => evaluateFilterPredicate(f, value)); } - if ('$not' in filter) { - return !evaluateEntityPredicate(filter.$not, value); + if ('$not' in predicate) { + return !evaluateFilterPredicate(predicate.$not, value); } - for (const filterKey in filter) { - if (!Object.hasOwn(filter, filterKey)) { + for (const filterKey in predicate) { + if (!Object.hasOwn(predicate, filterKey)) { continue; } if (filterKey.startsWith('$')) { return false; } if ( - !evaluatePredicateValue(filter[filterKey], valueAtPath(value, filterKey)) + !evaluateFilterPredicateValue( + predicate[filterKey], + getJsonValueAtPath(value as JsonValue, filterKey), + ) ) { return false; } @@ -69,13 +66,24 @@ function evaluateEntityPredicate( } /** - * Evaluate a single value against a predicate value. + * Convert a filter predicate to a filter function. + * + * @public + */ +export function filterPredicateToFilterFunction( + predicate: FilterPredicate, +): (value: T) => boolean { + return value => evaluateFilterPredicate(predicate, value); +} + +/** + * Evaluate a single value against a filter predicate value. * * @internal */ -function evaluatePredicateValue( - filter: EntityPredicateValue, - value: JsonValue | undefined, +function evaluateFilterPredicateValue( + filter: FilterPredicateValue, + value: unknown, ): boolean { if (typeof filter !== 'object' || filter === null || Array.isArray(filter)) { return valuesAreEqual(value, filter); @@ -85,7 +93,7 @@ function evaluatePredicateValue( if (!Array.isArray(value)) { return false; } - return value.some(v => evaluateEntityPredicate(filter.$contains, v)); + return value.some(v => evaluateFilterPredicate(filter.$contains, v)); } if ('$in' in filter) { return filter.$in.some(search => valuesAreEqual(value, search)); @@ -96,14 +104,19 @@ function evaluatePredicateValue( } return value === undefined; } + if ('$startsWith' in filter) { + if (typeof value !== 'string') { + return false; + } + return value + .toLocaleUpperCase('en-US') + .startsWith(filter.$startsWith.toLocaleUpperCase('en-US')); + } return false; } -function valuesAreEqual( - a: JsonValue | undefined, - b: JsonValue | undefined, -): boolean { +function valuesAreEqual(a: unknown, b: unknown): boolean { if (a === null || b === null) { return false; } diff --git a/plugins/catalog-react/src/alpha/predicates/valueAtPath.test.ts b/packages/filter-predicates/src/predicates/getJsonValueAtPath.test.ts similarity index 92% rename from plugins/catalog-react/src/alpha/predicates/valueAtPath.test.ts rename to packages/filter-predicates/src/predicates/getJsonValueAtPath.test.ts index 7ea563e4a1..40421232f4 100644 --- a/plugins/catalog-react/src/alpha/predicates/valueAtPath.test.ts +++ b/packages/filter-predicates/src/predicates/getJsonValueAtPath.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { valueAtPath } from './valueAtPath'; +import { getJsonValueAtPath } from './getJsonValueAtPath'; -describe('valueAtPath', () => { +describe('getJsonValueAtPath', () => { const subject = { name: 'Test', fields: { @@ -58,6 +58,6 @@ describe('valueAtPath', () => { ['mixed.annotations.example.com/description', 'A test subject'], ['mixed.annotations.long.domain.example.com/custom', 'long'], ])(`should find value at path %s`, (path, expected) => { - expect(valueAtPath(subject, path)).toEqual(expected); + expect(getJsonValueAtPath(subject, path)).toEqual(expected); }); }); diff --git a/plugins/catalog-react/src/alpha/predicates/valueAtPath.ts b/packages/filter-predicates/src/predicates/getJsonValueAtPath.ts similarity index 95% rename from plugins/catalog-react/src/alpha/predicates/valueAtPath.ts rename to packages/filter-predicates/src/predicates/getJsonValueAtPath.ts index acb46995d8..2dc982d896 100644 --- a/plugins/catalog-react/src/alpha/predicates/valueAtPath.ts +++ b/packages/filter-predicates/src/predicates/getJsonValueAtPath.ts @@ -27,9 +27,9 @@ import { JsonValue } from '@backstage/types'; * * This lookup does not traverse into arrays, returning `undefined` instead. * - * @internal + * @public */ -export function valueAtPath( +export function getJsonValueAtPath( value: JsonValue | undefined, path: string, ): JsonValue | undefined { @@ -55,7 +55,7 @@ export function valueAtPath( } } if (path.startsWith(`${valueKey}.`)) { - const found = valueAtPath( + const found = getJsonValueAtPath( value[valueKey], path.slice(valueKey.length + 1), ); diff --git a/packages/filter-predicates/src/predicates/index.ts b/packages/filter-predicates/src/predicates/index.ts new file mode 100644 index 0000000000..5684c7e62f --- /dev/null +++ b/packages/filter-predicates/src/predicates/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + readFilterPredicateFromConfig, + readOptionalFilterPredicateFromConfig, +} from './config'; +export type { ReadFilterPredicateFromConfigOptions } from './config'; +export { + evaluateFilterPredicate, + filterPredicateToFilterFunction, +} from './evaluate'; +export { getJsonValueAtPath } from './getJsonValueAtPath'; +export { + createZodV3FilterPredicateSchema, + parseFilterPredicate, +} from './schema'; +export type { + FilterPredicate, + FilterPredicateExpression, + FilterPredicatePrimitive, + FilterPredicateValue, + UnknownFilterPredicateOperator, + UnknownFilterPredicateValueMatcher, +} from './types'; diff --git a/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.test.ts b/packages/filter-predicates/src/predicates/schema.test.ts similarity index 50% rename from plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.test.ts rename to packages/filter-predicates/src/predicates/schema.test.ts index 3553fa932b..47f77bbce8 100644 --- a/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.test.ts +++ b/packages/filter-predicates/src/predicates/schema.test.ts @@ -15,14 +15,17 @@ */ import { z } from 'zod'; -import { createEntityPredicateSchema } from './createEntityPredicateSchema'; -import { EntityPredicate } from './types'; +import { + createZodV3FilterPredicateSchema, + parseFilterPredicate, +} from './schema'; +import { FilterPredicate } from './types'; -describe('createEntityPredicateSchema', () => { - const schema = createEntityPredicateSchema(z); +describe('createZodV3FilterPredicateSchema', () => { + const schema = createZodV3FilterPredicateSchema(z); describe('valid predicates', () => { - const predicates: EntityPredicate[] = [ + const predicates: FilterPredicate[] = [ 'string', '', 1, @@ -88,7 +91,7 @@ describe('createEntityPredicateSchema', () => { describe('invalid predicates', () => { const predicates: Array< - Exclude + Exclude > = [ [], ['foo', 'bar'], @@ -113,3 +116,104 @@ describe('createEntityPredicateSchema', () => { }); }); }); + +describe('parseFilterPredicate', () => { + describe('valid predicates', () => { + const predicates: FilterPredicate[] = [ + 'string', + '', + 1, + { kind: 'component', 'spec.type': 'service' }, + { 'metadata.tags': { $in: ['java'] } }, + { 'metadata.tags': { $contains: 'java' } }, + { + $all: [ + { 'metadata.tags': { $contains: 'java' } }, + { 'metadata.tags': { $contains: 'spring' } }, + ], + }, + { 'metadata.tags': { $in: ['go'] } }, + { 'metadata.tags.0': 'java' }, + { $not: { 'metadata.tags': { $in: ['java'] } } }, + { + $any: [ + { kind: 'component', 'spec.type': 'service' }, + { kind: 'group' }, + ], + }, + { + relations: { + $contains: { type: 'ownedBy', targetRef: 'group:default/g' }, + }, + }, + { + metadata: { $contains: { name: 'a' } }, + }, + { kind: 'component', 'spec.type': { $in: ['service', 'website'] } }, + { + $any: [ + { + $all: [ + { + kind: 'component', + 'spec.type': { $in: ['service', 'website'] }, + }, + ], + }, + { $all: [{ kind: 'api', 'spec.type': 'grpc' }] }, + ], + }, + { kind: 'component', 'spec.type': { $in: ['service'] } }, + { 'spec.owner': { $exists: true } }, + { 'spec.owner': { $exists: false } }, + { 'spec.type': 'service' }, + { $not: { 'spec.type': 'service' } }, + { + kind: 'component', + 'metadata.annotations.github.com/repo': { $exists: true }, + }, + { $all: [{ x: { $exists: true } }] }, + { $any: [{ x: { $exists: true } }] }, + { $not: { x: { $exists: true } } }, + { $not: { $all: [{ x: { $exists: true } }] } }, + ]; + + it.each(predicates)( + 'should return the predicate for valid input %j', + predicate => { + expect(parseFilterPredicate(predicate)).toEqual(predicate); + }, + ); + }); + + describe('invalid predicates', () => { + const predicates: Array< + Exclude + > = [ + [], + ['foo', 'bar'], + { kind: { 1: 'foo' } }, + { kind: { foo: 'bar' } }, + { kind: { $unknown: 'foo' } }, + { kind: { $in: 'foo' } }, + { kind: { $in: [{ x: 'foo' }] } }, + { kind: { $in: [{ x: 'foo' }] } }, + { 'spec.type': null }, + { $all: [{ x: { $unknown: true } }] }, + { $any: [{ x: { $unknown: true } }] }, + { $not: { x: { $unknown: true } } }, + { $not: { $all: [{ x: { $unknown: true } }] } }, + { $unknown: 'foo' }, + { 'metadata.tags': ['foo', 'bar'] }, + ]; + + it.each(predicates)( + 'should throw InputError for invalid predicate %j', + predicate => { + expect(() => parseFilterPredicate(predicate)).toThrow( + /Invalid filter predicate/, + ); + }, + ); + }); +}); diff --git a/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.ts b/packages/filter-predicates/src/predicates/schema.ts similarity index 53% rename from plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.ts rename to packages/filter-predicates/src/predicates/schema.ts index 3e31ec4d35..9684660219 100644 --- a/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.ts +++ b/packages/filter-predicates/src/predicates/schema.ts @@ -14,31 +14,39 @@ * limitations under the License. */ +import { InputError } from '@backstage/errors'; +import { fromZodError } from 'zod-validation-error/v3'; +import * as zodV3 from 'zod/v3'; import { - EntityPredicate, - EntityPredicateExpression, - EntityPredicatePrimitive, - EntityPredicateValue, + FilterPredicate, + FilterPredicateExpression, + FilterPredicatePrimitive, + FilterPredicateValue, } from './types'; -import type { z as zImpl, ZodType } from 'zod'; -/** @internal */ -export function createEntityPredicateSchema(z: typeof zImpl) { +/** + * Create a Zod schema for validating filter predicates. + * + * @public + */ +export function createZodV3FilterPredicateSchema( + z: typeof zodV3.z, +): zodV3.ZodType { const primitiveSchema = z.union([ z.string(), z.number(), z.boolean(), - ]) as ZodType; + ]) as zodV3.ZodType; // eslint-disable-next-line prefer-const - let valuePredicateSchema: ZodType; + let valuePredicateSchema: zodV3.ZodType; const expressionSchema = z.lazy(() => z.union([ z.record(z.string().regex(/^(?!\$).*$/), valuePredicateSchema), z.record(z.string().regex(/^\$/), z.never()), ]), - ) as ZodType; + ) as zodV3.ZodType; const predicateSchema = z.lazy(() => z.union([ @@ -48,14 +56,33 @@ export function createEntityPredicateSchema(z: typeof zImpl) { z.object({ $any: z.array(predicateSchema) }), z.object({ $not: predicateSchema }), ]), - ) as ZodType; + ) as zodV3.ZodType; valuePredicateSchema = z.union([ primitiveSchema, z.object({ $exists: z.boolean() }), z.object({ $in: z.array(primitiveSchema) }), z.object({ $contains: predicateSchema }), - ]) as ZodType; + ]) as zodV3.ZodType; return predicateSchema; } + +/** + * Parses a value to check that it's a valid filter predicate. + * + * @public + * @param value - The value to parse. + * @returns A valid filter predicate. + * @throws An error if the value is not a valid filter predicate. + */ +export function parseFilterPredicate(value: unknown): FilterPredicate { + const schema = createZodV3FilterPredicateSchema(zodV3.z); + const result = schema.safeParse(value); + if (!result.success) { + throw new InputError( + `Invalid filter predicate: ${fromZodError(result.error)}`, + ); + } + return result.data; +} diff --git a/packages/filter-predicates/src/predicates/types.ts b/packages/filter-predicates/src/predicates/types.ts new file mode 100644 index 0000000000..c66cdd8f64 --- /dev/null +++ b/packages/filter-predicates/src/predicates/types.ts @@ -0,0 +1,200 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JsonValue } from '@backstage/types'; + +/** + * Represents future additions to the set of filter predicate operators. + * + * @remarks + * + * If you write code that explicitly inspects filter predicate expressions, you + * should be ready for the appearance of such operators and deliberately + * gracefully fail to match them. + * + * @public + */ +export type UnknownFilterPredicateOperator = { + [KOperator in `$${string}`]: JsonValue; +} & { + [KOperator in '$all' | '$any' | '$not']: never; +}; + +/** + * A filter predicate that can be evaluated against a value. + * + * @remarks + * + * A predicate is always an object at the root. The most basic use case is to + * declare keys that are dot-separated paths into a structured data value, and + * values that all need to match the corresponding property value in the data. + * + * The equality test is case-insensitive and numbers are converted to strings, + * i.e. `"Component"` and `"component"` are considered equal, and so are `7` and + * `"7"`. + * + * Example that matches catalog entity components that are of type `service`: + * + * ```json + * { + * "kind": "Component", + * "spec.type": "service" + * } + * ``` + * + * The special keys `$all`, `$any`, and `$not` are logical operators that can be + * used to combine multiple predicates or negate their result. These must be + * used standalone, i.e. they cannot be combined with other matchers. + * + * Example: + * + * ```json + * { + * "$all": [ + * { + * "kind": "Component" + * }, + * { + * "$any": [{ "spec.type": "service" }, { "spec.type": "website" }] + * } + * ] + * } + * ``` + * + * Objects with the special keys `$exists`, `$in`, and `$contains` are value + * operators that can be used to perform more advanced matching against property + * values than just equality. + * + * Example: + * + * ```json + * { + * "filter": { + * "kind": "Component", + * "spec.type": { "$in": ["service", "website"] }, + * "metadata.annotations.github.com/project-slug": { $exists: true } + * } + * } + * ``` + * + * @public + */ +export type FilterPredicate = + | FilterPredicateExpression + | FilterPredicatePrimitive + | { + /** + * Asserts that all of the given predicates must be true. + */ + $all: FilterPredicate[]; + } + | { + /** + * Asserts that at least one of the given predicates must be true. + */ + $any: FilterPredicate[]; + } + | { + /** + * Asserts that the given predicate must not be true. + */ + $not: FilterPredicate; + } + | UnknownFilterPredicateOperator; + +/** + * A filter predicate expression that matches against one or more object + * properties. + * + * @remarks + * + * Each key of a record is a dot-separated path into the entity structure, e.g. + * `metadata.name`. + * + * The values are filter predicates that are evaluated against the value of the + * property at the given path. + * + * For values that are given as primitives, the equality test is + * case-insensitive and numbers are converted to strings, i.e. `"Component"` and + * `"component"` are considered equal, and so are `7` and `"7"`. + * + * @public + */ +export type FilterPredicateExpression = { + [KPath in string]: FilterPredicateValue; +} & { + [KPath in `$${string}`]: never; +}; + +/** + * Represents future additions to the set of filter predicate value matchers. + * + * @remarks + * + * If you write code that explicitly inspects filter predicate expressions, you + * should be ready for the appearance of such matchers and deliberately + * gracefully fail to match them. + * + * @public + */ +export type UnknownFilterPredicateValueMatcher = { + [KMatcher in `$${string}`]: JsonValue; +} & { + [KMatcher in '$exists' | '$in' | '$contains' | '$startsWith']: never; +}; + +/** + * A filter predicate value that can be used to match against a property value. + * + * @public + */ +export type FilterPredicateValue = + | FilterPredicatePrimitive + | { + /** + * Asserts that the property exists and has any value (`true`) - or not + * (`false`). + */ + $exists: boolean; + } + | { + /** + * Asserts that the property value is any one of the given possible + * values. + */ + $in: FilterPredicatePrimitive[]; + } + | { + /** + * Asserts that the property value is an array, and that at least one of + * its elements matches the given predicate. + */ + $contains: FilterPredicate; + } + | { + /** + * Asserts that the property value is string, and that it starts with the given string. + */ + $startsWith: string; + } + | UnknownFilterPredicateValueMatcher; + +/** + * A primitive value that can be used in filter predicates. + * + * @public + */ +export type FilterPredicatePrimitive = string | number | boolean; diff --git a/packages/filter-predicates/src/setupTests.ts b/packages/filter-predicates/src/setupTests.ts new file mode 100644 index 0000000000..e512e4ba83 --- /dev/null +++ b/packages/filter-predicates/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/api-docs/report-alpha.api.md b/plugins/api-docs/report-alpha.api.md index 2b42b528aa..c96d529b96 100644 --- a/plugins/api-docs/report-alpha.api.md +++ b/plugins/api-docs/report-alpha.api.md @@ -9,10 +9,10 @@ import { ApiFactory } from '@backstage/frontend-plugin-api'; import { defaultEntityContentGroups } from '@backstage/plugin-catalog-react/alpha'; import { Entity } from '@backstage/catalog-model'; import { EntityCardType } from '@backstage/plugin-catalog-react/alpha'; -import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { IconComponent } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -85,11 +85,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'consumed-apis'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -118,7 +118,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -126,11 +126,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'consuming-components'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -159,7 +159,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -167,11 +167,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'definition'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -200,7 +200,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -208,11 +208,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'has-apis'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -241,7 +241,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -249,11 +249,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'provided-apis'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -282,7 +282,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -290,11 +290,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'providing-components'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -323,7 +323,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -333,11 +333,11 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -384,7 +384,7 @@ const _default: OverridableFrontendPlugin< group?: keyof defaultEntityContentGroups | (string & {}); loader: () => Promise; routeRef?: RouteRef_2; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; }>; 'entity-content:api-docs/definition': OverridableExtensionDefinition<{ @@ -393,11 +393,11 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -444,7 +444,7 @@ const _default: OverridableFrontendPlugin< group?: keyof defaultEntityContentGroups | (string & {}); loader: () => Promise; routeRef?: RouteRef_2; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; }>; 'nav-item:api-docs': OverridableExtensionDefinition<{ diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index fce2dc1bec..55d96f485d 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -8,10 +8,10 @@ import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { EntityCardType } from '@backstage/plugin-catalog-react/alpha'; -import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -91,7 +91,7 @@ const _default: OverridableFrontendPlugin< curve: 'curveStepBefore' | 'curveMonotoneX' | undefined; title: string | undefined; height: number | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { @@ -106,7 +106,7 @@ const _default: OverridableFrontendPlugin< mergeRelations?: boolean | undefined; relationPairs?: [string, string][] | undefined; unidirectional?: boolean | undefined; - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -137,7 +137,7 @@ const _default: OverridableFrontendPlugin< name: 'relations'; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 584fe79014..361cb7cf8c 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -67,6 +67,7 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/filter-predicates": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/frontend-test-utils": "workspace:^", "@backstage/integration-react": "workspace:^", diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index d4cdf44742..10c36f448e 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -10,8 +10,8 @@ import { Entity } from '@backstage/catalog-model'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { IconLinkVerticalProps } from '@backstage/core-components'; -import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; import { ReactNode } from 'react'; import { ResourcePermission } from '@backstage/plugin-permission-common'; @@ -130,7 +130,7 @@ export function convertLegacyEntityCardExtension( LegacyExtension: ComponentType<{}>, overrides?: { name?: string; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }, ): ExtensionDefinition; @@ -140,7 +140,7 @@ export function convertLegacyEntityContentExtension( LegacyExtension: ComponentType<{}>, overrides?: { name?: string; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); path?: string; title?: string; defaultPath?: [Error: `Use the 'path' override instead`]; @@ -163,7 +163,7 @@ export const EntityCardBlueprint: ExtensionBlueprint<{ kind: 'entity-card'; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; output: @@ -191,11 +191,11 @@ export const EntityCardBlueprint: ExtensionBlueprint<{ >; inputs: {}; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; dataRefs: { @@ -232,7 +232,7 @@ export const EntityContentBlueprint: ExtensionBlueprint<{ group?: keyof typeof defaultEntityContentGroups | (string & {}); loader: () => Promise; routeRef?: RouteRef; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; output: | ExtensionDataRef @@ -270,11 +270,11 @@ export const EntityContentBlueprint: ExtensionBlueprint<{ config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -307,7 +307,7 @@ export const EntityContentBlueprint: ExtensionBlueprint<{ export const EntityContentLayoutBlueprint: ExtensionBlueprint<{ kind: 'entity-content-layout'; params: { - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); loader: () => Promise<(props: EntityContentLayoutProps) => JSX_2.Element>; }; output: @@ -333,10 +333,10 @@ export const EntityContentLayoutBlueprint: ExtensionBlueprint<{ inputs: {}; config: { type: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: string | undefined; }; dataRefs: { @@ -382,10 +382,10 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ >; inputs: {}; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; }; dataRefs: { filterFunction: ConfigurableExtensionDataRef< @@ -400,7 +400,7 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ export type EntityContextMenuItemParams = { useProps: UseProps; icon: JSX_2.Element; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; // @alpha (undocumented) @@ -408,7 +408,7 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ kind: 'entity-header'; params: { loader: () => Promise; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; output: | ExtensionDataRef< @@ -434,10 +434,10 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ >; inputs: {}; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; }; dataRefs: { filterFunction: ConfigurableExtensionDataRef< @@ -458,7 +458,7 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{ kind: 'entity-icon-link'; params: { useProps: () => Omit; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; output: | ExtensionDataRef< @@ -484,10 +484,10 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{ config: { label: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; label?: string | undefined; title?: string | undefined; }; @@ -510,48 +510,6 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{ }; }>; -// @alpha (undocumented) -export type EntityPredicate = - | EntityPredicateExpression - | EntityPredicatePrimitive - | { - $all: EntityPredicate[]; - } - | { - $any: EntityPredicate[]; - } - | { - $not: EntityPredicate; - }; - -// @alpha (undocumented) -export type EntityPredicateExpression = { - [KPath in string]: EntityPredicateValue; -} & { - [KPath in `$${string}`]: never; -}; - -// @alpha (undocumented) -export type EntityPredicatePrimitive = string | number | boolean; - -// @alpha -export function entityPredicateToFilterFunction( - entityPredicate: EntityPredicate, -): (value: T) => boolean; - -// @alpha (undocumented) -export type EntityPredicateValue = - | EntityPredicatePrimitive - | { - $exists: boolean; - } - | { - $in: EntityPredicatePrimitive[]; - } - | { - $contains: EntityPredicate; - }; - // @alpha (undocumented) export const EntityTableColumnTitle: ({ translationKey, diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts index fed6ced70f..7717b6a1f9 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts @@ -26,8 +26,10 @@ import { entityCardTypes, EntityCardType, } from './extensionData'; -import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; -import { EntityPredicate } from '../predicates'; +import { + FilterPredicate, + createZodV3FilterPredicateSchema, +} from '@backstage/filter-predicates'; import { resolveEntityFilterData } from './resolveEntityFilterData'; import { Entity } from '@backstage/catalog-model'; @@ -52,7 +54,7 @@ export const EntityCardBlueprint = createExtensionBlueprint({ config: { schema: { filter: z => - z.union([z.string(), createEntityPredicateSchema(z)]).optional(), + z.union([z.string(), createZodV3FilterPredicateSchema(z)]).optional(), type: z => z.enum(entityCardTypes).optional(), }, }, @@ -63,7 +65,7 @@ export const EntityCardBlueprint = createExtensionBlueprint({ type, }: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }, { node, config }, diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.ts b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.ts index 40fbbdf0f8..661c942f9d 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.ts +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.ts @@ -27,9 +27,11 @@ import { entityContentGroupDataRef, defaultEntityContentGroups, } from './extensionData'; -import { EntityPredicate } from '../predicates/types'; +import { + FilterPredicate, + createZodV3FilterPredicateSchema, +} from '@backstage/filter-predicates'; import { resolveEntityFilterData } from './resolveEntityFilterData'; -import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; import { Entity } from '@backstage/catalog-model'; /** @@ -59,7 +61,7 @@ export const EntityContentBlueprint = createExtensionBlueprint({ path: z => z.string().optional(), title: z => z.string().optional(), filter: z => - z.union([z.string(), createEntityPredicateSchema(z)]).optional(), + z.union([z.string(), createZodV3FilterPredicateSchema(z)]).optional(), group: z => z.literal(false).or(z.string()).optional(), }, }, @@ -82,7 +84,7 @@ export const EntityContentBlueprint = createExtensionBlueprint({ group?: keyof typeof defaultEntityContentGroups | (string & {}); loader: () => Promise; routeRef?: RouteRef; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }, { node, config }, ) { diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentLayoutBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContentLayoutBlueprint.tsx index c13ea8ad56..203c075e10 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContentLayoutBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentLayoutBlueprint.tsx @@ -25,9 +25,11 @@ import { EntityCardType, } from './extensionData'; import { JSX } from 'react'; -import { EntityPredicate } from '../predicates/types'; +import { + FilterPredicate, + createZodV3FilterPredicateSchema, +} from '@backstage/filter-predicates'; import { resolveEntityFilterData } from './resolveEntityFilterData'; -import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; import { Entity } from '@backstage/catalog-model'; /** @alpha */ @@ -62,7 +64,7 @@ export const EntityContentLayoutBlueprint = createExtensionBlueprint({ schema: { type: z => z.string().optional(), filter: z => - z.union([z.string(), createEntityPredicateSchema(z)]).optional(), + z.union([z.string(), createZodV3FilterPredicateSchema(z)]).optional(), }, }, *factory( @@ -70,7 +72,7 @@ export const EntityContentLayoutBlueprint = createExtensionBlueprint({ loader, filter, }: { - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); loader: () => Promise<(props: EntityContentLayoutProps) => JSX.Element>; }, { node, config }, diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx index 4b9b166196..19e46e0234 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -24,11 +24,13 @@ import MenuItem from '@material-ui/core/MenuItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import { useEntityContextMenu } from '../../hooks/useEntityContextMenu'; -import { EntityPredicate } from '../predicates/types'; -import { entityPredicateToFilterFunction } from '../predicates/entityPredicateToFilterFunction'; +import { + FilterPredicate, + filterPredicateToFilterFunction, + createZodV3FilterPredicateSchema, +} from '@backstage/filter-predicates'; import type { Entity } from '@backstage/catalog-model'; import { entityFilterFunctionDataRef } from './extensionData'; -import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; /** @alpha */ export type UseProps = () => | { @@ -46,7 +48,7 @@ export type UseProps = () => export type EntityContextMenuItemParams = { useProps: UseProps; icon: JSX.Element; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; /** @alpha */ @@ -62,7 +64,7 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ }, config: { schema: { - filter: z => createEntityPredicateSchema(z).optional(), + filter: z => createZodV3FilterPredicateSchema(z).optional(), }, }, *factory(params: EntityContextMenuItemParams, { node, config }) { @@ -98,13 +100,13 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ if (config.filter) { yield entityFilterFunctionDataRef( - entityPredicateToFilterFunction(config.filter), + filterPredicateToFilterFunction(config.filter), ); } else if (typeof params.filter === 'function') { yield entityFilterFunctionDataRef(params.filter); } else if (params.filter) { yield entityFilterFunctionDataRef( - entityPredicateToFilterFunction(params.filter), + filterPredicateToFilterFunction(params.filter), ); } }, diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx index b83136dcd9..401ffea316 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx @@ -19,10 +19,12 @@ import { coreExtensionData, ExtensionBoundary, } from '@backstage/frontend-plugin-api'; -import { EntityPredicate } from '../predicates/types'; +import { + FilterPredicate, + createZodV3FilterPredicateSchema, +} from '@backstage/filter-predicates'; import { Entity } from '@backstage/catalog-model'; import { resolveEntityFilterData } from './resolveEntityFilterData'; -import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; import { entityFilterExpressionDataRef, entityFilterFunctionDataRef, @@ -38,7 +40,7 @@ export const EntityHeaderBlueprint = createExtensionBlueprint({ }, config: { schema: { - filter: z => createEntityPredicateSchema(z).optional(), + filter: z => createZodV3FilterPredicateSchema(z).optional(), }, }, output: [ @@ -49,7 +51,7 @@ export const EntityHeaderBlueprint = createExtensionBlueprint({ *factory( params: { loader: () => Promise; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }, { node, config }, ) { diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx index 2368945c65..c71aea1e18 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx @@ -20,8 +20,10 @@ import { createExtensionDataRef, } from '@backstage/frontend-plugin-api'; -import { EntityPredicate } from '../predicates/types'; -import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; +import { + FilterPredicate, + createZodV3FilterPredicateSchema, +} from '@backstage/filter-predicates'; import { entityFilterExpressionDataRef, @@ -54,13 +56,13 @@ export const EntityIconLinkBlueprint = createExtensionBlueprint({ schema: { label: z => z.string().optional(), title: z => z.string().optional(), - filter: z => createEntityPredicateSchema(z).optional(), + filter: z => createZodV3FilterPredicateSchema(z).optional(), }, }, *factory( params: { useProps: () => Omit; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }, { config, node }, ) { diff --git a/plugins/catalog-react/src/alpha/blueprints/resolveEntityFilterData.ts b/plugins/catalog-react/src/alpha/blueprints/resolveEntityFilterData.ts index 75b7784c18..50ca39f99d 100644 --- a/plugins/catalog-react/src/alpha/blueprints/resolveEntityFilterData.ts +++ b/plugins/catalog-react/src/alpha/blueprints/resolveEntityFilterData.ts @@ -19,15 +19,15 @@ import { entityFilterFunctionDataRef, } from './extensionData'; import { - EntityPredicate, - entityPredicateToFilterFunction, -} from '../predicates'; + FilterPredicate, + filterPredicateToFilterFunction, +} from '@backstage/filter-predicates'; import { Entity } from '@backstage/catalog-model'; import { AppNode } from '@backstage/frontend-plugin-api'; export function* resolveEntityFilterData( - filter: ((entity: Entity) => boolean) | EntityPredicate | string | undefined, - config: { filter?: EntityPredicate | string }, + filter: ((entity: Entity) => boolean) | FilterPredicate | string | undefined, + config: { filter?: FilterPredicate | string }, node: AppNode, ) { if (typeof config.filter === 'string') { @@ -38,7 +38,7 @@ export function* resolveEntityFilterData( yield entityFilterExpressionDataRef(config.filter); } else if (config.filter) { yield entityFilterFunctionDataRef( - entityPredicateToFilterFunction(config.filter), + filterPredicateToFilterFunction(config.filter), ); } else if (typeof filter === 'function') { yield entityFilterFunctionDataRef(filter); @@ -49,6 +49,6 @@ export function* resolveEntityFilterData( ); yield entityFilterExpressionDataRef(filter); } else if (filter) { - yield entityFilterFunctionDataRef(entityPredicateToFilterFunction(filter)); + yield entityFilterFunctionDataRef(filterPredicateToFilterFunction(filter)); } } diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx index 9b343db469..5e6e91e251 100644 --- a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx @@ -20,7 +20,7 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; import { EntityCardBlueprint } from '../blueprints/EntityCardBlueprint'; import kebabCase from 'lodash/kebabCase'; -import { EntityPredicate } from '../predicates/types'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { Entity } from '@backstage/catalog-model'; import { EntityCardType } from '../blueprints/extensionData'; @@ -29,7 +29,7 @@ export function convertLegacyEntityCardExtension( LegacyExtension: ComponentType<{}>, overrides?: { name?: string; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }, ): ExtensionDefinition { diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx index 605ee96d71..295b8143d2 100644 --- a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx @@ -28,7 +28,7 @@ import kebabCase from 'lodash/kebabCase'; import startCase from 'lodash/startCase'; import { ComponentType } from 'react'; import { EntityContentBlueprint } from '../blueprints/EntityContentBlueprint'; -import { EntityPredicate } from '../predicates/types'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { Entity } from '@backstage/catalog-model'; /** @alpha */ @@ -36,7 +36,7 @@ export function convertLegacyEntityContentExtension( LegacyExtension: ComponentType<{}>, overrides?: { name?: string; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); path?: string; title?: string; diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index f5909c9aa2..6d2d76fd9c 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -16,7 +16,6 @@ export * from './blueprints'; export * from './converters'; -export * from './predicates'; export { catalogReactTranslationRef } from '../translation'; export { isOwnerOf } from '../utils/isOwnerOf'; export { useEntityPermission } from '../hooks/useEntityPermission'; diff --git a/plugins/catalog-react/src/alpha/predicates/types.ts b/plugins/catalog-react/src/alpha/predicates/types.ts deleted file mode 100644 index f19026c01b..0000000000 --- a/plugins/catalog-react/src/alpha/predicates/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** @alpha */ -export type EntityPredicate = - | EntityPredicateExpression - | EntityPredicatePrimitive - | { $all: EntityPredicate[] } - | { $any: EntityPredicate[] } - | { $not: EntityPredicate }; - -/** @alpha */ -export type EntityPredicateExpression = { - [KPath in string]: EntityPredicateValue; -} & { - [KPath in `$${string}`]: never; -}; - -/** @alpha */ -export type EntityPredicateValue = - | EntityPredicatePrimitive - | { $exists: boolean } - | { $in: EntityPredicatePrimitive[] } - | { $contains: EntityPredicate }; - -/** @alpha */ -export type EntityPredicatePrimitive = string | number | boolean; diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 7f68ba4c72..6dc72b1bb3 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -12,11 +12,11 @@ import { Entity } from '@backstage/catalog-model'; import { EntityCardType } from '@backstage/plugin-catalog-react/alpha'; import { EntityContentLayoutProps } from '@backstage/plugin-catalog-react/alpha'; import { EntityContextMenuItemParams } from '@backstage/plugin-catalog-react/alpha'; -import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { IconComponent } from '@backstage/frontend-plugin-api'; import { IconLinkVerticalProps } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; @@ -292,11 +292,11 @@ const _default: OverridableFrontendPlugin< }>; 'entity-card:catalog/about': OverridableExtensionDefinition<{ config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -354,7 +354,7 @@ const _default: OverridableFrontendPlugin< name: 'about'; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -362,11 +362,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'depends-on-components'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -395,7 +395,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -403,11 +403,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'depends-on-resources'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -436,7 +436,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -444,11 +444,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'has-components'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -477,7 +477,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -485,11 +485,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'has-resources'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -518,7 +518,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -526,11 +526,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'has-subcomponents'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -559,7 +559,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -567,11 +567,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'has-subdomains'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -600,7 +600,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -608,11 +608,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'has-systems'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -641,7 +641,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -649,11 +649,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'labels'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -682,7 +682,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -690,11 +690,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'links'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -723,7 +723,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -731,11 +731,11 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -840,17 +840,17 @@ const _default: OverridableFrontendPlugin< group?: keyof defaultEntityContentGroups | (string & {}); loader: () => Promise; routeRef?: RouteRef_2; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; }>; 'entity-context-menu-item:catalog/copy-entity-url': OverridableExtensionDefinition<{ kind: 'entity-context-menu-item'; name: 'copy-entity-url'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; }; output: | ExtensionDataRef @@ -868,10 +868,10 @@ const _default: OverridableFrontendPlugin< kind: 'entity-context-menu-item'; name: 'inspect-entity'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; }; output: | ExtensionDataRef @@ -889,10 +889,10 @@ const _default: OverridableFrontendPlugin< kind: 'entity-context-menu-item'; name: 'unregister-entity'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; }; output: | ExtensionDataRef @@ -912,10 +912,10 @@ const _default: OverridableFrontendPlugin< config: { label: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; label?: string | undefined; title?: string | undefined; }; @@ -942,7 +942,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { useProps: () => Omit; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; }>; 'nav-item:catalog': OverridableExtensionDefinition<{ diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index 9b847b862a..963dc12f22 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -8,9 +8,9 @@ import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; import { defaultEntityContentGroups } from '@backstage/plugin-catalog-react/alpha'; import { Entity } from '@backstage/catalog-model'; -import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -91,11 +91,11 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -142,7 +142,7 @@ const _default: OverridableFrontendPlugin< group?: keyof defaultEntityContentGroups | (string & {}); loader: () => Promise; routeRef?: RouteRef_2; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; }>; 'page:kubernetes': OverridableExtensionDefinition<{ diff --git a/plugins/org/report-alpha.api.md b/plugins/org/report-alpha.api.md index f6c3dcff2b..781cfe605a 100644 --- a/plugins/org/report-alpha.api.md +++ b/plugins/org/report-alpha.api.md @@ -5,9 +5,9 @@ ```ts import { Entity } from '@backstage/catalog-model'; import { EntityCardType } from '@backstage/plugin-catalog-react/alpha'; -import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -24,11 +24,11 @@ const _default: OverridableFrontendPlugin< kind: 'entity-card'; name: 'group-profile'; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -57,7 +57,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -65,13 +65,13 @@ const _default: OverridableFrontendPlugin< config: { initialRelationAggregation: 'direct' | 'aggregated' | undefined; showAggregateMembersToggle: boolean | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { showAggregateMembersToggle?: boolean | undefined; initialRelationAggregation?: 'direct' | 'aggregated' | undefined; - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -102,7 +102,7 @@ const _default: OverridableFrontendPlugin< name: 'members-list'; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -111,14 +111,14 @@ const _default: OverridableFrontendPlugin< initialRelationAggregation: 'direct' | 'aggregated' | undefined; showAggregateMembersToggle: boolean | undefined; ownedKinds: string[] | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { showAggregateMembersToggle?: boolean | undefined; initialRelationAggregation?: 'direct' | 'aggregated' | undefined; ownedKinds?: string[] | undefined; - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -149,7 +149,7 @@ const _default: OverridableFrontendPlugin< name: 'ownership'; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -157,13 +157,13 @@ const _default: OverridableFrontendPlugin< config: { maxRelations: number | undefined; hideIcons: boolean; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; type: 'content' | 'info' | undefined; }; configInput: { hideIcons?: boolean | undefined; maxRelations?: number | undefined; - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; type?: 'content' | 'info' | undefined; }; output: @@ -194,7 +194,7 @@ const _default: OverridableFrontendPlugin< name: 'user-profile'; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index b90bc4c693..09169ec789 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -10,12 +10,12 @@ import { ApiRef } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { FormField } from '@backstage/plugin-scaffolder-react/alpha'; import type { FormProps as FormProps_2 } from '@rjsf/core'; import { FormProps as FormProps_3 } from '@backstage/plugin-scaffolder-react'; @@ -137,10 +137,10 @@ const _default: OverridableFrontendPlugin< config: { label: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; label?: string | undefined; title?: string | undefined; }; @@ -167,7 +167,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { useProps: () => Omit; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; }>; 'nav-item:scaffolder': OverridableExtensionDefinition<{ diff --git a/plugins/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md index 493d1500da..eb2abbb132 100644 --- a/plugins/techdocs/report-alpha.api.md +++ b/plugins/techdocs/report-alpha.api.md @@ -9,10 +9,10 @@ import { ApiFactory } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { defaultEntityContentGroups } from '@backstage/plugin-catalog-react/alpha'; import { Entity } from '@backstage/catalog-model'; -import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { IconComponent } from '@backstage/frontend-plugin-api'; import { IconLinkVerticalProps } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; @@ -124,11 +124,11 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -204,7 +204,7 @@ const _default: OverridableFrontendPlugin< group?: keyof defaultEntityContentGroups | (string & {}); loader: () => Promise; routeRef?: RouteRef_2; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; }>; 'entity-icon-link:techdocs/read-docs': OverridableExtensionDefinition<{ @@ -213,10 +213,10 @@ const _default: OverridableFrontendPlugin< config: { label: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; label?: string | undefined; title?: string | undefined; }; @@ -243,7 +243,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { useProps: () => Omit; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; }>; 'nav-item:techdocs': OverridableExtensionDefinition<{ diff --git a/yarn.lock b/yarn.lock index 00462f7c47..7a4b186507 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3816,6 +3816,19 @@ __metadata: languageName: unknown linkType: soft +"@backstage/filter-predicates@workspace:^, @backstage/filter-predicates@workspace:packages/filter-predicates": + version: 0.0.0-use.local + resolution: "@backstage/filter-predicates@workspace:packages/filter-predicates" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/types": "workspace:^" + zod: "npm:^3.25.76" + zod-validation-error: "npm:^4.0.2" + languageName: unknown + linkType: soft + "@backstage/frontend-app-api@workspace:^, @backstage/frontend-app-api@workspace:packages/frontend-app-api": version: 0.0.0-use.local resolution: "@backstage/frontend-app-api@workspace:packages/frontend-app-api" @@ -5391,6 +5404,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/integration-react": "workspace:^"