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..76edeb61d2 --- /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; + } + | { + $hasPrefix: 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' | '$hasPrefix']: 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..6eb54c3c92 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': { $hasPrefix: '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..2006265004 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 ('$hasPrefix' in filter) { + if (typeof value !== 'string') { + return false; + } + return value + .toLocaleUpperCase('en-US') + .startsWith(filter.$hasPrefix.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..8c5b298719 --- /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' | '$hasPrefix']: 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. + */ + $hasPrefix: 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 33cb3e6f76..3865a53d16 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 { JSXElementConstructor } from 'react'; @@ -87,11 +87,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: @@ -120,7 +120,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -128,11 +128,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: @@ -161,7 +161,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -169,11 +169,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: @@ -202,7 +202,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -210,11 +210,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: @@ -243,7 +243,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -251,11 +251,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: @@ -284,7 +284,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -292,11 +292,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: @@ -325,7 +325,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -335,12 +335,12 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; icon: string | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -396,7 +396,7 @@ const _default: OverridableFrontendPlugin< icon?: string | ReactElement; loader: () => Promise; routeRef?: RouteRef_2; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; }>; 'entity-content:api-docs/definition': OverridableExtensionDefinition<{ @@ -405,12 +405,12 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; icon: string | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -466,7 +466,7 @@ const _default: OverridableFrontendPlugin< icon?: string | ReactElement; 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 908a0c470e..081cfce740 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/integration-react": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 3ad5833a9f..74779de6db 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 { JSXElementConstructor } from 'react'; import { ReactElement } from 'react'; @@ -132,7 +132,7 @@ export function convertLegacyEntityCardExtension( LegacyExtension: ComponentType<{}>, overrides?: { name?: string; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }, ): ExtensionDefinition; @@ -142,7 +142,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; icon?: string | ReactElement; @@ -184,7 +184,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: @@ -212,11 +212,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: { @@ -254,7 +254,7 @@ export const EntityContentBlueprint: ExtensionBlueprint<{ icon?: string | ReactElement; loader: () => Promise; routeRef?: RouteRef; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; output: | ExtensionDataRef @@ -299,12 +299,12 @@ export const EntityContentBlueprint: ExtensionBlueprint<{ config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; icon: string | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -352,7 +352,7 @@ export type EntityContentGroupDefinitions = Record< 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: @@ -378,10 +378,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: { @@ -427,10 +427,10 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ >; inputs: {}; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; }; dataRefs: { filterFunction: ConfigurableExtensionDataRef< @@ -445,7 +445,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) @@ -453,7 +453,7 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ kind: 'entity-header'; params: { loader: () => Promise; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; output: | ExtensionDataRef< @@ -479,10 +479,10 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ >; inputs: {}; config: { - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; }; dataRefs: { filterFunction: ConfigurableExtensionDataRef< @@ -503,7 +503,7 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{ kind: 'entity-icon-link'; params: { useProps: () => Omit; - filter?: EntityPredicate | ((entity: Entity) => boolean); + filter?: FilterPredicate | ((entity: Entity) => boolean); }; output: | ExtensionDataRef< @@ -529,10 +529,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; }; @@ -555,48 +555,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 dde2517095..65896c7833 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.ts +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.ts @@ -28,9 +28,11 @@ import { defaultEntityContentGroups, entityContentIconDataRef, } 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'; import { ReactElement } from 'react'; @@ -63,7 +65,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(), icon: z => z.string().optional(), }, @@ -88,7 +90,7 @@ export const EntityContentBlueprint = createExtensionBlueprint({ icon?: string | ReactElement; 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 e929845a2c..6113bdeca2 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, ReactElement } 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; icon?: string | ReactElement; 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 ebea4f4617..5815313e17 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'; @@ -304,11 +304,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: @@ -366,7 +366,7 @@ const _default: OverridableFrontendPlugin< name: 'about'; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -374,11 +374,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: @@ -407,7 +407,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -415,11 +415,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: @@ -448,7 +448,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -456,11 +456,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: @@ -489,7 +489,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -497,11 +497,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: @@ -530,7 +530,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -538,11 +538,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: @@ -571,7 +571,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -579,11 +579,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: @@ -612,7 +612,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -620,11 +620,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: @@ -653,7 +653,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -661,11 +661,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: @@ -694,7 +694,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -702,11 +702,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: @@ -735,7 +735,7 @@ const _default: OverridableFrontendPlugin< inputs: {}; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); type?: EntityCardType; }; }>; @@ -743,12 +743,12 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; icon: string | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -862,17 +862,17 @@ const _default: OverridableFrontendPlugin< icon?: string | ReactElement; 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 @@ -890,10 +890,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 @@ -911,10 +911,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 @@ -934,10 +934,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; }; @@ -964,7 +964,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 020c9580d2..46a576191f 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 { JSXElementConstructor } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -93,12 +93,12 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; icon: string | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -154,7 +154,7 @@ const _default: OverridableFrontendPlugin< icon?: string | ReactElement; 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 4d83a737aa..62c027b2ad 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 db712d02b5..d839a03f01 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'; @@ -126,12 +126,12 @@ const _default: OverridableFrontendPlugin< config: { path: string | undefined; title: string | undefined; - filter: EntityPredicate | undefined; + filter: FilterPredicate | undefined; group: string | false | undefined; icon: string | undefined; }; configInput: { - filter?: EntityPredicate | undefined; + filter?: FilterPredicate | undefined; title?: string | undefined; path?: string | undefined; group?: string | false | undefined; @@ -216,7 +216,7 @@ const _default: OverridableFrontendPlugin< icon?: string | ReactElement; loader: () => Promise; routeRef?: RouteRef_2; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: string | FilterPredicate | ((entity: Entity) => boolean); }; }>; 'entity-icon-link:techdocs/read-docs': OverridableExtensionDefinition<{ @@ -225,10 +225,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; }; @@ -255,7 +255,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 f90a8202c4..9149a9e58d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3824,6 +3824,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" @@ -5414,6 +5427,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:^"