Merge pull request #15063 from rikez/feat/enhance-owner-picker
feat(scaffolder): add EntityFilterQuery support on Entity and Owner Picker components
This commit is contained in:
@@ -73,16 +73,16 @@ spec:
|
||||
description: Owner of the component
|
||||
ui:field: OwnerPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- Group
|
||||
catalogFilter:
|
||||
kind: Group
|
||||
system:
|
||||
title: System
|
||||
type: string
|
||||
description: System of the component
|
||||
ui:field: EntityPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- System
|
||||
catalogFilter:
|
||||
kind: System
|
||||
defaultKind: System
|
||||
|
||||
- title: Choose a location
|
||||
|
||||
@@ -74,16 +74,16 @@ spec:
|
||||
description: Owner of the component
|
||||
ui:field: OwnerPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- Group
|
||||
catalogFilter:
|
||||
kind: Group
|
||||
system:
|
||||
title: System
|
||||
type: string
|
||||
description: System of the component
|
||||
ui:field: EntityPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- System
|
||||
catalogFilter:
|
||||
kind: System
|
||||
defaultKind: System
|
||||
|
||||
- title: Choose a location
|
||||
|
||||
@@ -73,16 +73,16 @@ spec:
|
||||
description: Owner of the component
|
||||
ui:field: OwnerPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- Group
|
||||
catalogFilter:
|
||||
kind: Group
|
||||
system:
|
||||
title: System
|
||||
type: string
|
||||
description: System of the component
|
||||
ui:field: EntityPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- System
|
||||
catalogFilter:
|
||||
kind: System
|
||||
defaultKind: System
|
||||
|
||||
- title: Choose a location
|
||||
|
||||
@@ -49,8 +49,8 @@ spec:
|
||||
description: Owner of the component
|
||||
ui:field: OwnerPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- Group
|
||||
catalogFilter:
|
||||
kind: Group
|
||||
|
||||
steps:
|
||||
- id: fetch-base
|
||||
|
||||
@@ -88,6 +88,10 @@ export const EntityPickerFieldExtension: FieldExtensionComponent<
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<string, string | string[]>
|
||||
| Record<string, string | string[]>[]
|
||||
| undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -99,6 +103,10 @@ export const EntityPickerFieldSchema: FieldSchema<
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<string, string | string[]>
|
||||
| Record<string, string | string[]>[]
|
||||
| undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -316,6 +324,10 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent<
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<string, string | string[]>
|
||||
| Record<string, string | string[]>[]
|
||||
| undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -326,6 +338,10 @@ export const OwnerPickerFieldSchema: FieldSchema<
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<string, string | string[]>
|
||||
| Record<string, string | string[]>[]
|
||||
| undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ parameters:
|
||||
description: Owner of the component
|
||||
ui:field: OwnerPicker
|
||||
ui:options:
|
||||
allowedKinds:
|
||||
- Group
|
||||
catalogFilter:
|
||||
kind: Group
|
||||
- title: Choose a location
|
||||
required:
|
||||
- repoUrl
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityFilterQuery } 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';
|
||||
@@ -34,7 +35,13 @@ describe('<EntityPicker />', () => {
|
||||
const schema = {};
|
||||
const required = false;
|
||||
let uiSchema: {
|
||||
'ui:options': { allowedKinds?: string[]; defaultKind?: string };
|
||||
'ui:options': {
|
||||
allowedKinds?: string[];
|
||||
defaultKind?: string;
|
||||
allowArbitraryValues?: boolean;
|
||||
defaultNamespace?: string | false;
|
||||
catalogFilter?: EntityFilterQuery;
|
||||
};
|
||||
};
|
||||
const rawErrors: string[] = [];
|
||||
const formData = undefined;
|
||||
@@ -66,7 +73,7 @@ describe('<EntityPicker />', () => {
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('without allowedKinds', () => {
|
||||
describe('without allowedKinds and catalogFilter', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = { 'ui:options': {} };
|
||||
props = {
|
||||
@@ -136,4 +143,97 @@ describe('<EntityPicker />', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with catalogFilter', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('searches for a specific group entity', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
{
|
||||
kind: ['User'],
|
||||
'metadata.name': 'test-entity',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('catalogFilter should take precedence over allowedKinds', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-group',
|
||||
},
|
||||
],
|
||||
allowedKinds: ['User'],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('searches for a Group entity', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'metadata.name': 'test-group',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { type EntityFilterQuery } from '@backstage/catalog-client';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
catalogApiRef,
|
||||
@@ -44,6 +45,11 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
idSchema,
|
||||
} = props;
|
||||
const allowedKinds = uiSchema['ui:options']?.allowedKinds;
|
||||
|
||||
const catalogFilter: EntityFilterQuery | undefined =
|
||||
uiSchema['ui:options']?.catalogFilter ||
|
||||
(allowedKinds && { kind: allowedKinds });
|
||||
|
||||
const defaultKind = uiSchema['ui:options']?.defaultKind;
|
||||
const defaultNamespace = uiSchema['ui:options']?.defaultNamespace;
|
||||
|
||||
@@ -51,7 +57,7 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
|
||||
const { value: entities, loading } = useAsync(() =>
|
||||
catalogApi.getEntities(
|
||||
allowedKinds ? { filter: { kind: allowedKinds } } : undefined,
|
||||
catalogFilter ? { filter: catalogFilter } : undefined,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -16,16 +16,28 @@
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const entityQueryFilterExpressionSchema = z.record(
|
||||
z.string().or(z.array(z.string())),
|
||||
);
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const EntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
/**
|
||||
* @deprecated Use `catalogFilter` instead.
|
||||
*/
|
||||
allowedKinds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('List of kinds of entities to derive options from'),
|
||||
.describe(
|
||||
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from',
|
||||
),
|
||||
defaultKind: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -42,6 +54,11 @@ export const EntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EntityFilterQuery } 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';
|
||||
@@ -32,7 +33,15 @@ describe('<OwnerPicker />', () => {
|
||||
const onChange = jest.fn();
|
||||
const schema = {};
|
||||
const required = false;
|
||||
let uiSchema: { 'ui:options': { allowedKinds?: string[] } };
|
||||
let uiSchema: {
|
||||
'ui:options': {
|
||||
allowedKinds?: string[];
|
||||
defaultKind?: string;
|
||||
allowArbitraryValues?: boolean;
|
||||
defaultNamespace?: string | false;
|
||||
catalogFilter?: EntityFilterQuery;
|
||||
};
|
||||
};
|
||||
const rawErrors: string[] = [];
|
||||
const formData = undefined;
|
||||
|
||||
@@ -63,7 +72,7 @@ describe('<OwnerPicker />', () => {
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('without allowedKinds', () => {
|
||||
describe('without catalogFilter and allowedKinds', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = { 'ui:options': {} };
|
||||
props = {
|
||||
@@ -108,7 +117,7 @@ describe('<OwnerPicker />', () => {
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('searches for users and groups', async () => {
|
||||
it('searches for users', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<OwnerPicker {...props} />
|
||||
@@ -122,4 +131,93 @@ describe('<OwnerPicker />', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with catalogFilter', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'spec.type': 'team',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('searches for group entities of type team', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<OwnerPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: ['Group'],
|
||||
'spec.type': 'team',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('catalogFilter should take precedence over allowedKinds', () => {
|
||||
beforeEach(() => {
|
||||
uiSchema = {
|
||||
'ui:options': {
|
||||
allowedKinds: ['User'],
|
||||
catalogFilter: [
|
||||
{
|
||||
kind: ['Group', 'User'],
|
||||
},
|
||||
{
|
||||
'spec.type': ['team', 'business-unit'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
props = {
|
||||
onChange,
|
||||
schema,
|
||||
required,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
} as unknown as FieldProps<any>;
|
||||
|
||||
catalogApi.getEntities.mockResolvedValue({ items: entities });
|
||||
});
|
||||
|
||||
it('searches for users and groups or teams and business units', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<OwnerPicker {...props} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: ['Group', 'User'],
|
||||
},
|
||||
{
|
||||
'spec.type': ['team', 'business-unit'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,14 +33,16 @@ export const OwnerPicker = (props: OwnerPickerProps) => {
|
||||
} = props;
|
||||
|
||||
const defaultNamespace = uiSchema['ui:options']?.defaultNamespace;
|
||||
const allowedKinds = uiSchema['ui:options']?.allowedKinds;
|
||||
|
||||
const catalogFilter = uiSchema['ui:options']?.catalogFilter || {
|
||||
kind: allowedKinds || ['Group', 'User'],
|
||||
};
|
||||
|
||||
const ownerUiSchema = {
|
||||
...uiSchema,
|
||||
'ui:options': {
|
||||
allowedKinds: (uiSchema['ui:options']?.allowedKinds || [
|
||||
'Group',
|
||||
'User',
|
||||
]) as string[],
|
||||
catalogFilter,
|
||||
defaultKind: 'Group',
|
||||
allowArbitraryValues:
|
||||
uiSchema['ui:options']?.allowArbitraryValues ?? true,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
import { entityQueryFilterExpressionSchema } from '../EntityPicker/schema';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -22,12 +23,15 @@ import { makeFieldSchemaFromZod } from '../utils';
|
||||
export const OwnerPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
/**
|
||||
* @deprecated Use `catalogFilter` instead.
|
||||
*/
|
||||
allowedKinds: z
|
||||
.array(z.string())
|
||||
.default(['Group', 'User'])
|
||||
.optional()
|
||||
.describe(
|
||||
'List of kinds of entities to derive options from. Defaults to Group and User',
|
||||
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from. Defaults to Group and User',
|
||||
),
|
||||
allowArbitraryValues: z
|
||||
.boolean()
|
||||
@@ -39,6 +43,11 @@ export const OwnerPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
.describe(
|
||||
'The default namespace. Options with this namespace will not be prefixed.',
|
||||
),
|
||||
catalogFilter: z
|
||||
.array(entityQueryFilterExpressionSchema)
|
||||
.or(entityQueryFilterExpressionSchema)
|
||||
.optional()
|
||||
.describe('List of key-value filter expression for entities'),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user