Merge pull request #16779 from Sarabadu/sarabadu/allow-catalog-filter-exist

[scaffolder] add support to CATALOG_FILTER_EXISTS v2
This commit is contained in:
Ben Lambert
2023-03-20 11:42:40 +01:00
committed by GitHub
6 changed files with 238 additions and 27 deletions
+14
View File
@@ -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
+64 -8
View File
@@ -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];