introduce the @backstage/filter-predicates package

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2026-02-03 14:36:56 +01:00
parent f2c1da4c4c
commit 7feb83b586
42 changed files with 1092 additions and 322 deletions
+11
View File
@@ -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.
+13
View File
@@ -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`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/filter-predicates': minor
---
Introduced package, basically as the extracted predicate types from `@backstage/plugin-catalog-react/alpha`
+4 -1
View File
@@ -186,4 +186,7 @@ docs.json
tsconfig.typedoc.tmp.json
# Storybook
dist-storybook/
dist-storybook/
# Personal allow patterns etc
.claude/settings.local.json
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+18
View File
@@ -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"] }
}
}
```
@@ -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
+48
View File
@@ -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:^"
}
}
+107
View File
@@ -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<FilterPredicate>;
// @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<T = unknown>(
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;
};
```
@@ -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';
@@ -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' }],
});
});
});
@@ -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);
}
@@ -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);
});
});
@@ -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<T extends JsonValue>(
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<T = unknown>(
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;
}
@@ -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);
});
});
@@ -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),
);
@@ -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';
@@ -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<EntityPredicate | unknown, EntityPredicate>
Exclude<FilterPredicate | unknown, FilterPredicate>
> = [
[],
['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<FilterPredicate | unknown, FilterPredicate>
> = [
[],
['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/,
);
},
);
});
});
@@ -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<FilterPredicate> {
const primitiveSchema = z.union([
z.string(),
z.number(),
z.boolean(),
]) as ZodType<EntityPredicatePrimitive>;
]) as zodV3.ZodType<FilterPredicatePrimitive>;
// eslint-disable-next-line prefer-const
let valuePredicateSchema: ZodType<EntityPredicateValue>;
let valuePredicateSchema: zodV3.ZodType<FilterPredicateValue>;
const expressionSchema = z.lazy(() =>
z.union([
z.record(z.string().regex(/^(?!\$).*$/), valuePredicateSchema),
z.record(z.string().regex(/^\$/), z.never()),
]),
) as ZodType<EntityPredicateExpression>;
) as zodV3.ZodType<FilterPredicateExpression>;
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<EntityPredicate>;
) as zodV3.ZodType<FilterPredicate>;
valuePredicateSchema = z.union([
primitiveSchema,
z.object({ $exists: z.boolean() }),
z.object({ $in: z.array(primitiveSchema) }),
z.object({ $contains: predicateSchema }),
]) as ZodType<EntityPredicateValue>;
]) as zodV3.ZodType<FilterPredicateValue>;
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;
}
@@ -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;
@@ -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 {};
+25 -25
View File
@@ -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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
routeRef?: RouteRef_2;
filter?: string | EntityPredicate | ((entity: Entity) => boolean);
filter?: string | FilterPredicate | ((entity: Entity) => boolean);
};
}>;
'nav-item:api-docs': OverridableExtensionDefinition<{
+4 -4
View File
@@ -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<JSX.Element>;
filter?: string | EntityPredicate | ((entity: Entity) => boolean);
filter?: string | FilterPredicate | ((entity: Entity) => boolean);
type?: EntityCardType;
};
}>;
+1
View File
@@ -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:^",
+21 -63
View File
@@ -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<JSX.Element>;
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<JSX.Element>;
routeRef?: RouteRef;
filter?: string | EntityPredicate | ((entity: Entity) => boolean);
filter?: string | FilterPredicate | ((entity: Entity) => boolean);
};
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
@@ -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<JSX.Element>;
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<IconLinkVerticalProps, 'color'>;
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<T extends JsonValue>(
entityPredicate: EntityPredicate,
): (value: T) => boolean;
// @alpha (undocumented)
export type EntityPredicateValue =
| EntityPredicatePrimitive
| {
$exists: boolean;
}
| {
$in: EntityPredicatePrimitive[];
}
| {
$contains: EntityPredicate;
};
// @alpha (undocumented)
export const EntityTableColumnTitle: ({
translationKey,
@@ -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<JSX.Element>;
filter?: string | EntityPredicate | ((entity: Entity) => boolean);
filter?: string | FilterPredicate | ((entity: Entity) => boolean);
type?: EntityCardType;
},
{ node, config },
@@ -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<JSX.Element>;
routeRef?: RouteRef;
filter?: string | EntityPredicate | ((entity: Entity) => boolean);
filter?: string | FilterPredicate | ((entity: Entity) => boolean);
},
{ node, config },
) {
@@ -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 },
@@ -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),
);
}
},
@@ -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<JSX.Element>;
filter?: EntityPredicate | ((entity: Entity) => boolean);
filter?: FilterPredicate | ((entity: Entity) => boolean);
},
{ node, config },
) {
@@ -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<IconLinkVerticalProps, 'color'>;
filter?: EntityPredicate | ((entity: Entity) => boolean);
filter?: FilterPredicate | ((entity: Entity) => boolean);
},
{ config, node },
) {
@@ -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));
}
}
@@ -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 {
@@ -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;
-1
View File
@@ -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';
@@ -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;
+43 -43
View File
@@ -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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX_2.Element, 'core.reactElement', {}>
@@ -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<JSX_2.Element, 'core.reactElement', {}>
@@ -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<JSX_2.Element, 'core.reactElement', {}>
@@ -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<IconLinkVerticalProps, 'color'>;
filter?: EntityPredicate | ((entity: Entity) => boolean);
filter?: FilterPredicate | ((entity: Entity) => boolean);
};
}>;
'nav-item:catalog': OverridableExtensionDefinition<{
+4 -4
View File
@@ -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<JSX.Element>;
routeRef?: RouteRef_2;
filter?: string | EntityPredicate | ((entity: Entity) => boolean);
filter?: string | FilterPredicate | ((entity: Entity) => boolean);
};
}>;
'page:kubernetes': OverridableExtensionDefinition<{
+13 -13
View File
@@ -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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
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<JSX.Element>;
filter?: string | EntityPredicate | ((entity: Entity) => boolean);
filter?: string | FilterPredicate | ((entity: Entity) => boolean);
type?: EntityCardType;
};
}>;
+4 -4
View File
@@ -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<IconLinkVerticalProps, 'color'>;
filter?: EntityPredicate | ((entity: Entity) => boolean);
filter?: FilterPredicate | ((entity: Entity) => boolean);
};
}>;
'nav-item:scaffolder': OverridableExtensionDefinition<{
+7 -7
View File
@@ -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<JSX.Element>;
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<IconLinkVerticalProps, 'color'>;
filter?: EntityPredicate | ((entity: Entity) => boolean);
filter?: FilterPredicate | ((entity: Entity) => boolean);
};
}>;
'nav-item:techdocs': OverridableExtensionDefinition<{
+14
View File
@@ -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:^"