Merge pull request #16779 from Sarabadu/sarabadu/allow-catalog-filter-exist
[scaffolder] add support to CATALOG_FILTER_EXISTS v2
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Allow use of `{ exists: true }` value inside filters to filter entities that has that key.
|
||||
|
||||
this example will filter all entities that has the annotation `someAnnotation` set to any value.
|
||||
|
||||
```yaml
|
||||
ui:options:
|
||||
catalogFilter:
|
||||
kind: Group
|
||||
metadata.annotations.someAnnotation: { exists: true }
|
||||
```
|
||||
@@ -472,6 +472,20 @@ owner:
|
||||
kind: [Group, User]
|
||||
```
|
||||
|
||||
#### `catalogFilter`
|
||||
|
||||
The `catalogFilter` allow you to filter the list entities using any of the [catalog api filters](https://backstage.io/docs/features/software-catalog/software-catalog-api#filtering):
|
||||
|
||||
For example, if you want to show users in the `default` namespace, and groups with the `github.com/team-slug` annotation, you can do the following:
|
||||
|
||||
```yaml
|
||||
catalogFilter:
|
||||
- kind: [User]
|
||||
metadata.namespace: default
|
||||
- kind: [Group]
|
||||
metadata.annotations.github.com/team-slug: { exists: true }
|
||||
```
|
||||
|
||||
## `spec.steps` - `Action[]`
|
||||
|
||||
The `steps` is an array of the things that you want to happen part of this
|
||||
|
||||
@@ -80,8 +80,22 @@ export const EntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<string, string | string[]>
|
||||
| Record<string, string | string[]>[]
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
exists?: boolean | undefined;
|
||||
}
|
||||
>
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
exists?: boolean | undefined;
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
}
|
||||
>;
|
||||
@@ -95,8 +109,22 @@ export const EntityPickerFieldSchema: FieldSchema<
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<string, string | string[]>
|
||||
| Record<string, string | string[]>[]
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
exists?: boolean | undefined;
|
||||
}
|
||||
>
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
exists?: boolean | undefined;
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
}
|
||||
>;
|
||||
@@ -212,8 +240,22 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent_2<
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<string, string | string[]>
|
||||
| Record<string, string | string[]>[]
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
exists?: boolean | undefined;
|
||||
}
|
||||
>
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
exists?: boolean | undefined;
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
}
|
||||
>;
|
||||
@@ -226,8 +268,22 @@ export const OwnerPickerFieldSchema: FieldSchema<
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<string, string | string[]>
|
||||
| Record<string, string | string[]>[]
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
exists?: boolean | undefined;
|
||||
}
|
||||
>
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
exists?: boolean | undefined;
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityFilterQuery } from '@backstage/catalog-client';
|
||||
import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
@@ -22,6 +22,7 @@ import { FieldProps } from '@rjsf/core';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { EntityPicker } from './EntityPicker';
|
||||
import { EntityPickerProps } from './schema';
|
||||
|
||||
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
@@ -34,15 +35,7 @@ describe('<EntityPicker />', () => {
|
||||
const onChange = jest.fn();
|
||||
const schema = {};
|
||||
const required = false;
|
||||
let uiSchema: {
|
||||
'ui:options': {
|
||||
allowedKinds?: string[];
|
||||
defaultKind?: string;
|
||||
allowArbitraryValues?: boolean;
|
||||
defaultNamespace?: string | false;
|
||||
catalogFilter?: EntityFilterQuery;
|
||||
};
|
||||
};
|
||||
let uiSchema: EntityPickerProps['uiSchema'];
|
||||
const rawErrors: string[] = [];
|
||||
const formData = undefined;
|
||||
|
||||
@@ -192,6 +185,59 @@ describe('<EntityPicker />', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
it('allow single top level filter', async () => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: {
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityPicker {...props} uiSchema={uiSchema} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: {
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('search for entitities containing an specific key', async () => {
|
||||
const uiSchemaWithBoolean = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.annotation.some/anotation': { exists: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityPicker {...props} uiSchema={uiSchemaWithBoolean} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.annotation.some/anotation': CATALOG_FILTER_EXISTS,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('catalogFilter should take precedence over allowedKinds', () => {
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { type EntityFilterQuery } from '@backstage/catalog-client';
|
||||
import {
|
||||
type EntityFilterQuery,
|
||||
CATALOG_FILTER_EXISTS,
|
||||
} from '@backstage/catalog-client';
|
||||
import {
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
@@ -31,7 +34,12 @@ import Autocomplete, {
|
||||
} from '@material-ui/lab/Autocomplete';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { EntityPickerProps } from './schema';
|
||||
import {
|
||||
EntityPickerFilterQueryValue,
|
||||
EntityPickerProps,
|
||||
EntityPickerUiOptions,
|
||||
EntityPickerFilterQuery,
|
||||
} from './schema';
|
||||
|
||||
export { EntityPickerSchema } from './schema';
|
||||
|
||||
@@ -51,12 +59,7 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
formData,
|
||||
idSchema,
|
||||
} = props;
|
||||
const allowedKinds = uiSchema['ui:options']?.allowedKinds;
|
||||
|
||||
const catalogFilter: EntityFilterQuery | undefined =
|
||||
uiSchema['ui:options']?.catalogFilter ||
|
||||
(allowedKinds && { kind: allowedKinds });
|
||||
|
||||
const catalogFilter = buildCatalogFilter(uiSchema);
|
||||
const defaultKind = uiSchema['ui:options']?.defaultKind;
|
||||
const defaultNamespace =
|
||||
uiSchema['ui:options']?.defaultNamespace || undefined;
|
||||
@@ -167,3 +170,71 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a especial `{exists: true}` value to the `CATALOG_FILTER_EXISTS` symbol.
|
||||
*
|
||||
* @param value - The value to convert.
|
||||
* @returns The converted value.
|
||||
*/
|
||||
function convertOpsValues(
|
||||
value: Exclude<EntityPickerFilterQueryValue, Array<any>>,
|
||||
): string | symbol {
|
||||
if (typeof value === 'object' && value.exists) {
|
||||
return CATALOG_FILTER_EXISTS;
|
||||
}
|
||||
return value?.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts schema filters to entity filter query, replacing `{exists:true}` values
|
||||
* with the constant `CATALOG_FILTER_EXISTS`.
|
||||
*
|
||||
* @param schemaFilters - An object containing schema filters with keys as filter names
|
||||
* and values as filter values.
|
||||
* @returns An object with the same keys as the input object, but with `{exists:true}` values
|
||||
* transformed to `CATALOG_FILTER_EXISTS` symbol.
|
||||
*/
|
||||
function convertSchemaFiltersToQuery(
|
||||
schemaFilters: EntityPickerFilterQuery,
|
||||
): Exclude<EntityFilterQuery, Array<any>> {
|
||||
const query: EntityFilterQuery = {};
|
||||
|
||||
for (const [key, value] of Object.entries(schemaFilters)) {
|
||||
if (Array.isArray(value)) {
|
||||
query[key] = value;
|
||||
} else {
|
||||
query[key] = convertOpsValues(value);
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an `EntityFilterQuery` based on the `uiSchema` passed in.
|
||||
* If `catalogFilter` is specified in the `uiSchema`, it is converted to a `EntityFilterQuery`.
|
||||
* If `allowedKinds` is specified in the `uiSchema` will support the legacy `allowedKinds` option.
|
||||
*
|
||||
* @param uiSchema The `uiSchema` of an `EntityPicker` component.
|
||||
* @returns An `EntityFilterQuery` based on the `uiSchema`, or `undefined` if `catalogFilter` is not specified in the `uiSchema`.
|
||||
*/
|
||||
function buildCatalogFilter(
|
||||
uiSchema: EntityPickerProps['uiSchema'],
|
||||
): EntityFilterQuery | undefined {
|
||||
const allowedKinds = uiSchema['ui:options']?.allowedKinds;
|
||||
|
||||
const catalogFilter: EntityPickerUiOptions['catalogFilter'] | undefined =
|
||||
uiSchema['ui:options']?.catalogFilter ||
|
||||
(allowedKinds && { kind: allowedKinds });
|
||||
|
||||
if (!catalogFilter) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (Array.isArray(catalogFilter)) {
|
||||
return catalogFilter.map(convertSchemaFiltersToQuery);
|
||||
}
|
||||
|
||||
return convertSchemaFiltersToQuery(catalogFilter);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ import { makeFieldSchemaFromZod } from '../utils';
|
||||
* @public
|
||||
*/
|
||||
export const entityQueryFilterExpressionSchema = z.record(
|
||||
z.string().or(z.array(z.string())),
|
||||
z
|
||||
.string()
|
||||
.or(z.object({ exists: z.boolean().optional() }))
|
||||
.or(z.array(z.string())),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -74,3 +77,10 @@ export type EntityPickerUiOptions =
|
||||
export type EntityPickerProps = typeof EntityPickerFieldSchema.type;
|
||||
|
||||
export const EntityPickerSchema = EntityPickerFieldSchema.schema;
|
||||
|
||||
export type EntityPickerFilterQuery = z.TypeOf<
|
||||
typeof entityQueryFilterExpressionSchema
|
||||
>;
|
||||
|
||||
export type EntityPickerFilterQueryValue =
|
||||
EntityPickerFilterQuery[keyof EntityPickerFilterQuery];
|
||||
|
||||
Reference in New Issue
Block a user