diff --git a/.changeset/perfect-buses-collect.md b/.changeset/perfect-buses-collect.md
new file mode 100644
index 0000000000..cd7b8164cf
--- /dev/null
+++ b/.changeset/perfect-buses-collect.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder': minor
+---
+
+Added OwnedEntityPicker field which displays Owned Entities in options
diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.test.tsx
new file mode 100644
index 0000000000..ef6fc1b648
--- /dev/null
+++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.test.tsx
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2021 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 { Entity } from '@backstage/catalog-model';
+import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
+import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
+import { FieldProps } from '@rjsf/core';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { OwnedEntityPicker } from './OwnedEntityPicker';
+
+const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
+ apiVersion: 'backstage.io/v1beta1',
+ kind,
+ metadata: { namespace, name },
+});
+
+describe('', () => {
+ let entities: Entity[];
+ const onChange = jest.fn();
+ const schema = {};
+ const required = false;
+ let uiSchema: {
+ 'ui:options': { allowedKinds?: string[]; defaultKind?: string };
+ };
+ const rawErrors: string[] = [];
+ const formData = undefined;
+
+ let props: FieldProps;
+
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(async () => ({ items: entities })),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ entities = [
+ makeEntity('Group', 'default', 'team-a'),
+ makeEntity('Group', 'default', 'squad-b'),
+ ];
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+
+ {children}
+
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ describe('without allowedKinds', () => {
+ beforeEach(() => {
+ uiSchema = { 'ui:options': {} };
+ props = {
+ onChange,
+ schema,
+ required,
+ uiSchema,
+ rawErrors,
+ formData,
+ } as unknown as FieldProps;
+
+ catalogApi.getEntities.mockResolvedValue({ items: entities });
+ });
+
+ it('searches for all entities', async () => {
+ await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(catalogApi.getEntities).toHaveBeenCalledWith(undefined);
+ });
+
+ it('updates even if there is not an exact match', async () => {
+ const { getByLabelText } = await renderInTestApp(
+
+
+ ,
+ );
+ const input = getByLabelText('Entity');
+
+ userEvent.type(input, 'squ');
+ input.blur();
+
+ expect(onChange).toHaveBeenCalledWith('squ');
+ });
+ });
+
+ describe('with allowedKinds', () => {
+ beforeEach(() => {
+ uiSchema = { 'ui:options': { allowedKinds: ['User'] } };
+ props = {
+ onChange,
+ schema,
+ required,
+ uiSchema,
+ rawErrors,
+ formData,
+ } as unknown as FieldProps;
+
+ catalogApi.getEntities.mockResolvedValue({ items: entities });
+ });
+
+ it('searches for users and groups', async () => {
+ await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(catalogApi.getEntities).toHaveBeenCalledWith({
+ filter: {
+ kind: ['User'],
+ },
+ });
+ });
+ });
+});
diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx
new file mode 100644
index 0000000000..b73158ac60
--- /dev/null
+++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2021 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 { useApi } from '@backstage/core-plugin-api';
+import {
+ catalogApiRef,
+ formatEntityRefTitle,
+ useEntityOwnership,
+} from '@backstage/plugin-catalog-react';
+import { TextField } from '@material-ui/core';
+import FormControl from '@material-ui/core/FormControl';
+import Autocomplete from '@material-ui/lab/Autocomplete';
+import { FieldProps } from '@rjsf/core';
+import React from 'react';
+import { useAsync } from 'react-use';
+
+export const OwnedEntityPicker = ({
+ onChange,
+ schema: { title = 'Entity', description = 'An entity from the catalog' },
+ required,
+ uiSchema,
+ rawErrors,
+ formData,
+ idSchema,
+}: FieldProps) => {
+ const allowedKinds = uiSchema['ui:options']?.allowedKinds as string[];
+ const defaultKind = uiSchema['ui:options']?.defaultKind as string | undefined;
+ const catalogApi = useApi(catalogApiRef);
+ const { isOwnedEntity } = useEntityOwnership();
+
+ const { value: entities, loading } = useAsync(() =>
+ catalogApi.getEntities(
+ allowedKinds ? { filter: { kind: allowedKinds } } : undefined,
+ ),
+ );
+
+ const entityRefs = entities?.items
+ .map(e =>
+ isOwnedEntity(e) ? formatEntityRefTitle(e, { defaultKind }) : null,
+ )
+ .filter(n => n);
+
+ const onSelect = (_: any, value: string | null) => {
+ onChange(value || '');
+ };
+ return (
+ 0 && !formData}
+ >
+ (
+
+ )}
+ />
+
+ );
+};
diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts
new file mode 100644
index 0000000000..7cf5add6eb
--- /dev/null
+++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2021 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 { OwnedEntityPicker } from './OwnedEntityPicker';
diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts
index 5adc3d3141..299470e220 100644
--- a/plugins/scaffolder/src/components/fields/index.ts
+++ b/plugins/scaffolder/src/components/fields/index.ts
@@ -18,3 +18,4 @@ export * from './EntityPicker';
export * from './OwnerPicker';
export * from './RepoUrlPicker';
export * from './TextValuePicker';
+export * from './OwnedEntityPicker';
diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts
index be934546b3..12a29809e0 100644
--- a/plugins/scaffolder/src/extensions/default.ts
+++ b/plugins/scaffolder/src/extensions/default.ts
@@ -24,6 +24,7 @@ import {
RepoUrlPicker,
} from '../components/fields/RepoUrlPicker';
import { FieldExtensionOptions } from './types';
+import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker';
export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [
{
@@ -44,4 +45,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [
component: OwnerPicker,
name: 'OwnerPicker',
},
+ {
+ component: OwnedEntityPicker,
+ name: 'OwnedEntityPicker',
+ },
];
diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts
index 0180c24b90..dfbd2af296 100644
--- a/plugins/scaffolder/src/index.ts
+++ b/plugins/scaffolder/src/index.ts
@@ -31,6 +31,7 @@ export {
EntityPickerFieldExtension,
EntityNamePickerFieldExtension,
OwnerPickerFieldExtension,
+ OwnedEntityPickerFieldExtension,
RepoUrlPickerFieldExtension,
ScaffolderPage,
scaffolderPlugin as plugin,
@@ -42,6 +43,7 @@ export {
OwnerPicker,
RepoUrlPicker,
TextValuePicker,
+ OwnedEntityPicker,
} from './components/fields';
export { FavouriteTemplate } from './components/FavouriteTemplate';
export { TemplateList } from './components/TemplateList';
diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts
index b6ef0ebe17..95cad50eba 100644
--- a/plugins/scaffolder/src/plugin.ts
+++ b/plugins/scaffolder/src/plugin.ts
@@ -35,6 +35,7 @@ import {
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
+import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker';
export const scaffolderPlugin = createPlugin({
id: 'scaffolder',
@@ -95,3 +96,10 @@ export const ScaffolderPage = scaffolderPlugin.provide(
mountPoint: rootRouteRef,
}),
);
+
+export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide(
+ createScaffolderFieldExtension({
+ component: OwnedEntityPicker,
+ name: 'OwnedEntityPicker',
+ }),
+);